app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
master
Drew Owens
Share frozen native Rust mechanical development checkpoint and qualification scope
b2d7cdb
27d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
use super::*;
use crate::mechanics::{DesignState, Target};
use fusion_mechanical_core::prepare_request;
use std::collections::{BTreeMap, BTreeSet};
const EPOCH: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
#[derive(Clone)]
pub(crate) struct Fake(pub(crate) Rc<RefCell<World>>);
pub(crate) struct World {
order: Vec<u64>,
nodes: BTreeMap<u64, Node>,
next: u64,
lease: u64,
leases: BTreeSet<u64>,
pub(crate) creates: usize,
pub(crate) closes: usize,
pub(crate) activations: usize,
parametric: usize,
command: String,
create_loss: bool,
return_old: bool,
post_read_failure: bool,
pub(crate) close_loss: bool,
close_stays_open: bool,
activation_wrong: bool,
}
struct Node {
document: Document,
valid: bool,
profile: ScratchProfile,
revision: String,
}
pub(crate) struct Handle {
object: u64,
lease: u64,
}
impl Fake {
pub(crate) fn new() -> Self {
let mut nodes = BTreeMap::new();
for id in [1, 2] {
nodes.insert(
id,
Node {
document: Document {
document_id: None,
name: "Untitled".into(),
creation_id: "copied-metadata".into(),
active: id == 1,
saved: false,
modified: false,
},
valid: true,
profile: blank(),
revision: "initial".into(),
},
);
}
Self(Rc::new(RefCell::new(World {
order: vec![1, 2],
nodes,
next: 3,
lease: 0,
leases: BTreeSet::new(),
creates: 0,
closes: 0,
activations: 0,
parametric: 0,
command: "SelectCommand".into(),
create_loss: false,
return_old: false,
post_read_failure: false,
close_loss: false,
close_stays_open: false,
activation_wrong: false,
})))
}
fn lease(&self, object: u64) -> Handle {
let mut w = self.0.borrow_mut();
w.lease += 1;
let lease = w.lease;
assert!(w.leases.insert(lease));
Handle { object, lease }
}
fn created(&self) -> u64 {
self.0.borrow().next - 1
}
}
fn blank() -> ScratchProfile {
ScratchProfile {
parametric: 1,
hybrid: 1,
components: 1,
..Default::default()
}
}
impl DocumentSdk for Fake {
type Handle = Handle;
fn version(&self) -> Result<String, String> {
Ok("instrumented".into())
}
fn thread_id(&self) -> u32 {
1
}
fn count(&self) -> Result<usize, String> {
Ok(self.0.borrow().order.len())
}
fn retain(&self, index: usize) -> Result<Handle, String> {
let id = *self.0.borrow().order.get(index).ok_or("missing index")?;
Ok(self.lease(id))
}
fn read(&self, h: &Handle) -> Result<Document, String> {
let w = self.0.borrow();
if w.post_read_failure && h.object > 2 {
return Err("injected read failure".into());
}
Ok(w.nodes
.get(&h.object)
.ok_or("missing document")?
.document
.clone())
}
fn same(&self, a: &Handle, b: &Handle) -> Result<bool, String> {
Ok(a.object == b.object)
}
fn is_valid(&self, h: &Handle) -> Result<bool, String> {
Ok(self
.0
.borrow()
.nodes
.get(&h.object)
.is_some_and(|n| n.valid))
}
fn release(&self, h: Handle) -> Result<(), String> {
assert!(
self.0.borrow_mut().leases.remove(&h.lease),
"release attempted twice"
);
Ok(())
}
}
impl LifecycleSdk for Fake {
fn active_command(&self) -> Result<String, String> {
Ok(self.0.borrow().command.clone())
}
fn create_blank(&self) -> CreationAttempt<Handle> {
let (id, loss) = {
let mut w = self.0.borrow_mut();
w.creates += 1;
if w.return_old {
(1, false)
} else {
let id = w.next;
w.next += 1;
for n in w.nodes.values_mut() {
n.document.active = false;
}
w.nodes.insert(
id,
Node {
document: Document {
document_id: None,
name: "Untitled".into(),
creation_id: "copied-metadata".into(),
active: true,
saved: false,
modified: false,
},
valid: true,
profile: blank(),
revision: format!("revision-{id}"),
},
);
w.order.push(id);
(id, w.create_loss)
}
};
CreationAttempt {
handle: Some(self.lease(id)),
invoked: true,
acknowledgement: if loss {
Err("lost creation ack".into())
} else {
Ok(())
},
}
}
fn make_parametric(&self, _: &Handle) -> EffectAttempt {
self.0.borrow_mut().parametric += 1;
EffectAttempt {
invoked: true,
acknowledgement: Ok(true),
}
}
fn scratch_profile(&self, h: &Handle) -> Result<ScratchProfile, String> {
Ok(self.0.borrow().nodes.get(&h.object).unwrap().profile)
}
fn has_data_file(&self, h: &Handle) -> Result<bool, String> {
Ok(self.0.borrow().nodes.get(&h.object).unwrap().document.saved)
}
fn inspect_mechanical(&self, h: &Handle, id: &str) -> Result<Inspection, String> {
let w = self.0.borrow();
let n = w.nodes.get(&h.object).unwrap();
Ok(Inspection {
target: Target {
document_id: id.into(),
revision: n.revision.clone(),
},
revision_scope: crate::mechanics::REVISION_SCOPE,
complete: false,
scope: "instrumented parameters",
design: DesignState {
active: n.document.active,
parametric: true,
root_name: "fixture".into(),
root_revision: n.revision.clone(),
default_length_units: "mm".into(),
component_count: 1,
body_count: n.profile.bodies,
sketch_count: 0,
timeline_count: 0,
timeline_position: 0,
},
parameters: vec![],
health: vec![],
})
}
fn discard_document(&self, h: &Handle) -> EffectAttempt {
let mut w = self.0.borrow_mut();
w.closes += 1;
if !w.close_stays_open {
w.nodes.get_mut(&h.object).unwrap().valid = false;
w.order.retain(|id| *id != h.object);
w.nodes.get_mut(&1).unwrap().document.active = true;
}
EffectAttempt {
invoked: true,
acknowledgement: if w.close_loss {
Err("lost close ack".into())
} else {
Ok(true)
},
}
}
fn activate_document(&self, h: &Handle) -> EffectAttempt {
let mut w = self.0.borrow_mut();
w.activations += 1;
let id = if w.activation_wrong { 1 } else { h.object };
for (key, n) in &mut w.nodes {
n.document.active = *key == id;
}
EffectAttempt {
invoked: true,
acknowledgement: Ok(true),
}
}
}
pub(crate) fn create_request(epoch: &str, operation: &str) -> PreparedRequest {
prepare_request(json!({"op":"new_design","sessionId":epoch,"operationId":operation,"name":"requested only"}),epoch).unwrap()
}
fn setup() -> (Registry<Fake>, Fake) {
let fake = Fake::new();
let registry = Registry::new(EPOCH.into(), fake.clone()).unwrap();
(registry, fake)
}
fn create(registry: &Registry<Fake>, operation: &str) -> (Value, CreationProof) {
let request = create_request(EPOCH, operation);
let plan = registry.lifecycle_preflight(&request, None).unwrap();
let value = registry.lifecycle_execute(plan);
assert_eq!(value["success"], true, "{value}");
let proof = CreationProof {
session: EPOCH.into(),
operation_id: operation.into(),
request_digest: fusion_mechanical_journal::request_digest(request.canonical_bytes()),
document_id: value["data"]["target"]["documentId"]
.as_str()
.unwrap()
.into(),
};
(value, proof)
}
fn discard_request(value: &Value, proof: &CreationProof) -> PreparedRequest {
prepare_request(json!({"op":"native_scratch_discard","sessionId":EPOCH,"operationId":"discard","creationOperationId":proof.operation_id,"target":value["data"]["target"]}),EPOCH).unwrap()
}
#[test]
fn exact_create_and_discard_ignore_copied_metadata_and_preserve_originals() {
let (registry, fake) = setup();
let before = registry.refresh().unwrap();
let (value, proof) = create(®istry, "create");
assert!(
value["data"]["target"]["documentId"].as_str().unwrap()
!= before.documents[0].document_id.as_ref().unwrap()
);
assert_eq!(value["data"]["document"]["name"], "Untitled");
assert_eq!(value["data"]["requestedName"], "requested only");
fake.0.borrow_mut().order.reverse(); // Order is not identity.
let plan = registry
.lifecycle_preflight(&discard_request(&value, &proof), Some(proof))
.unwrap();
let closed = registry.lifecycle_execute(plan);
assert_eq!(closed["success"], true, "{closed}");
assert!(fake.0.borrow().nodes[&1].valid && fake.0.borrow().nodes[&2].valid);
assert_eq!(fake.0.borrow().closes, 1);
registry.close().unwrap();
assert!(fake.0.borrow().leases.is_empty());
}
#[test]
fn existing_document_return_cannot_be_adopted_or_modified() {
let (registry, fake) = setup();
fake.0.borrow_mut().return_old = true;
let plan = registry
.lifecycle_preflight(&create_request(EPOCH, "create"), None)
.unwrap();
let result = registry.lifecycle_execute(plan);
assert_eq!(result["outcome"], "unknown");
assert_eq!(fake.0.borrow().parametric, 0);
assert!(registry.is_faulted());
registry.close().unwrap();
assert!(fake.0.borrow().leases.is_empty());
}
#[test]
fn lost_create_ack_or_post_create_read_failure_never_replays_or_auto_closes() {
for lost_ack in [true, false] {
let (registry, fake) = setup();
let request = create_request(EPOCH, "create");
let plan = registry.lifecycle_preflight(&request, None).unwrap();
if lost_ack {
fake.0.borrow_mut().create_loss = true;
} else {
fake.0.borrow_mut().post_read_failure = true;
}
let result = registry.lifecycle_execute(plan);
assert_eq!(result["outcome"], "unknown");
assert!(registry.lifecycle_preflight(&request, None).is_err());
assert_eq!(fake.0.borrow().creates, 1);
assert_eq!(fake.0.borrow().closes, 0);
assert!(!fake.0.borrow().leases.is_empty());
registry.close().unwrap();
assert!(fake.0.borrow().leases.is_empty());
}
}
#[test]
fn discard_requires_live_owner_and_matching_current_session_proof() {
let (registry, fake) = setup();
let (value, proof) = create(®istry, "create");
let request = discard_request(&value, &proof);
assert!(registry.lifecycle_preflight(&request, None).is_err());
for change in 0..4 {
let mut bad = proof.clone();
match change {
0 => bad.session = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".into(),
1 => bad.document_id = "other".into(),
2 => bad.request_digest = "wrong".into(),
_ => bad.operation_id = "other".into(),
};
assert!(registry.lifecycle_preflight(&request, Some(bad)).is_err());
}
assert_eq!(fake.0.borrow().closes, 0);
registry.close().unwrap();
}
#[test]
fn renamed_saved_geometry_and_stale_revision_refuse_before_close() {
for change in 0..4 {
let (registry, fake) = setup();
let (value, proof) = create(®istry, "create");
let id = fake.created();
{
let mut w = fake.0.borrow_mut();
let n = w.nodes.get_mut(&id).unwrap();
match change {
0 => n.document.name = "user work".into(),
1 => n.document.saved = true,
2 => n.profile.bodies = 1,
_ => n.revision = "changed".into(),
}
}
assert!(registry
.lifecycle_preflight(&discard_request(&value, &proof), Some(proof))
.is_err());
assert_eq!(fake.0.borrow().closes, 0);
registry.close().unwrap();
}
}
#[test]
fn second_hop_rechecks_baseline_command_and_parameter_revision() {
for change in 0..3 {
let (registry, fake) = setup();
let (value, proof) = create(®istry, "create");
let plan = registry
.lifecycle_preflight(&discard_request(&value, &proof), Some(proof))
.unwrap();
let id = fake.created();
{
let mut w = fake.0.borrow_mut();
match change {
0 => w.nodes.get_mut(&1).unwrap().document.modified = true,
1 => w.command = "SketchCommand".into(),
_ => w.nodes.get_mut(&id).unwrap().revision = "new expression".into(),
}
}
let result = registry.lifecycle_execute(plan);
assert_eq!(result["nativeExecutionStarted"], false);
assert_eq!(fake.0.borrow().closes, 0);
registry.close().unwrap();
}
}
#[test]
fn uncertain_close_is_spent_even_when_document_disappeared() {
for stays in [true, false] {
let (registry, fake) = setup();
let (value, proof) = create(®istry, "create");
let request = discard_request(&value, &proof);
let plan = registry
.lifecycle_preflight(&request, Some(proof.clone()))
.unwrap();
{
let mut w = fake.0.borrow_mut();
w.close_loss = true;
w.close_stays_open = stays;
}
let result = registry.lifecycle_execute(plan);
assert_eq!(result["outcome"], "unknown");
assert!(registry.lifecycle_preflight(&request, Some(proof)).is_err());
assert_eq!(fake.0.borrow().closes, 1);
registry.close().unwrap();
}
}
#[test]
fn activation_uses_exact_id_and_requires_exact_active_readback() {
for wrong in [false, true] {
let (registry, fake) = setup();
let snapshot = registry.refresh().unwrap();
let id = snapshot.documents[1].document_id.clone().unwrap();
let request=prepare_request(json!({"op":"native_document_activate","sessionId":EPOCH,"operationId":"activate","target":{"documentId":id},"expectedActiveDocumentId":snapshot.documents[0].document_id}),EPOCH).unwrap();
let plan = registry.lifecycle_preflight(&request, None).unwrap();
fake.0.borrow_mut().activation_wrong = wrong;
let result = registry.lifecycle_execute(plan);
assert_eq!(result["success"], !wrong);
assert_eq!(fake.0.borrow().activations, 1);
registry.close().unwrap();
}
}
#[test]
fn bounded_owned_slots_and_serial_exhaustion_precede_creation() {
let (registry, fake) = setup();
for i in 0..MAX_OWNED_SCRATCH {
create(®istry, &format!("create-{i}"));
}
assert!(registry
.lifecycle_preflight(&create_request(EPOCH, "one-too-many"), None)
.is_err());
assert_eq!(fake.0.borrow().creates, MAX_OWNED_SCRATCH);
registry.close().unwrap();
let (registry, fake) = setup();
registry.state.borrow_mut().next = u64::MAX;
assert!(registry
.lifecycle_preflight(&create_request(EPOCH, "overflow"), None)
.is_err());
assert_eq!(fake.0.borrow().creates, 0);
registry.close().unwrap();
}
#[test]
fn unsupported_box_refuses_before_any_sdk_effect() {
let (registry, fake) = setup();
let request=prepare_request(json!({"op":"new_design","sessionId":EPOCH,"operationId":"box","box":{"width":1,"height":2,"depth":3}}),EPOCH).unwrap();
assert!(supported_arguments(&request).is_err());
assert!(registry.lifecycle_preflight(&request, None).is_err());
assert_eq!(fake.0.borrow().creates, 0);
registry.close().unwrap();
}
#[test]
fn externally_replaced_same_metadata_document_never_inherits_scratch_ownership() {
let (registry, fake) = setup();
let (value, proof) = create(®istry, "create");
let old = fake.created();
{
let mut w = fake.0.borrow_mut();
let document = w.nodes[&old].document.clone();
w.nodes.get_mut(&old).unwrap().valid = false;
w.order.retain(|id| *id != old);
let id = w.next;
w.next += 1;
w.nodes.insert(
id,
Node {
document,
valid: true,
profile: blank(),
revision: "replacement".into(),
},
);
w.order.push(id);
}
assert!(registry
.lifecycle_preflight(&discard_request(&value, &proof), Some(proof))
.is_err());
assert_eq!(fake.0.borrow().closes, 0);
registry.close().unwrap();
assert!(fake.0.borrow().leases.is_empty());
}
#[test]
fn document_member_capacity_refuses_before_create_and_stop_releases_only_leases() {
let (registry, fake) = setup();
{
let mut w = fake.0.borrow_mut();
for id in 3..=MAX_DOCUMENTS as u64 {
w.nodes.insert(
id,
Node {
document: Document {
document_id: None,
name: format!("Untitled {id}"),
creation_id: format!("creation {id}"),
active: false,
saved: false,
modified: false,
},
valid: true,
profile: blank(),
revision: "initial".into(),
},
);
w.order.push(id);
}
}
assert!(registry
.lifecycle_preflight(&create_request(EPOCH, "full"), None)
.is_err());
assert_eq!(fake.0.borrow().creates, 0);
registry.close().unwrap();
assert_eq!(fake.0.borrow().closes, 0);
assert_eq!(fake.0.borrow().order.len(), MAX_DOCUMENTS);
assert!(fake.0.borrow().leases.is_empty());
}