Integrate release recovery, semantic review, coverage and Windows packaging
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
//! Typed authored workflow inputs; no stored command is executable on recovery.
|
||||
use super::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
enum Saved {
|
||||
Semantic(serde_json::Value),
|
||||
Ports(serde_json::Value),
|
||||
Relation {
|
||||
base: String,
|
||||
id: Id,
|
||||
name: String,
|
||||
kind: RelationKind,
|
||||
source: Option<Id>,
|
||||
target: Option<Id>,
|
||||
condition: String,
|
||||
layer: Layer,
|
||||
items: Vec<Id>,
|
||||
source_port: PortChoice,
|
||||
target_port: PortChoice,
|
||||
oriented: bool,
|
||||
},
|
||||
ManualTrace {
|
||||
base: String,
|
||||
name: String,
|
||||
source: Id,
|
||||
target: Option<Id>,
|
||||
},
|
||||
Transition {
|
||||
base: String,
|
||||
request: TransitionRequest,
|
||||
},
|
||||
}
|
||||
pub(crate) fn endpoint(p: &Project, port: PortChoice) -> EndpointChoice {
|
||||
let label = match port {
|
||||
PortChoice::Auto => "Automatic · reuse only if unique".into(),
|
||||
PortChoice::Create => "Create a new port".into(),
|
||||
PortChoice::Reuse(id) => format!("Reuse {}", choice(p, id).label),
|
||||
};
|
||||
EndpointChoice {
|
||||
choice: port,
|
||||
label,
|
||||
}
|
||||
}
|
||||
fn check(p: &Project, base: &str) -> Result<(), String> {
|
||||
if project_fingerprint(p) != base {
|
||||
return Err(
|
||||
"Workflow recovery does not match the accepted model identity and revision.".into(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
impl Workflow {
|
||||
pub fn recovery_snapshot(&self) -> Result<serde_json::Value, String> {
|
||||
let saved = match self {
|
||||
Self::Semantic(state) => Saved::Semantic(state.recovery_snapshot()?),
|
||||
Self::Ports(state) => Saved::Ports(state.recovery_snapshot()?),
|
||||
Self::Relation(d) => Saved::Relation {
|
||||
base: d.base_fingerprint.clone(),
|
||||
id: d.id,
|
||||
name: d.name.clone(),
|
||||
kind: d.kind,
|
||||
source: d.source.as_ref().map(|c| c.id),
|
||||
target: d.target.as_ref().map(|c| c.id),
|
||||
condition: d.condition.text(),
|
||||
layer: d.layer,
|
||||
items: d.items.clone(),
|
||||
source_port: d.source_port.choice,
|
||||
target_port: d.target_port.choice,
|
||||
oriented: d.oriented,
|
||||
},
|
||||
Self::ManualTrace {
|
||||
base_fingerprint,
|
||||
name,
|
||||
source,
|
||||
target,
|
||||
} => Saved::ManualTrace {
|
||||
base: base_fingerprint.clone(),
|
||||
name: name.clone(),
|
||||
source: source.id,
|
||||
target: target.as_ref().map(|c| c.id),
|
||||
},
|
||||
Self::Transition(d) => Saved::Transition {
|
||||
base: d.plan.base_fingerprint.clone(),
|
||||
request: d.request.clone(),
|
||||
},
|
||||
};
|
||||
serde_json::to_value(saved).map_err(|e| e.to_string())
|
||||
}
|
||||
pub fn from_recovery(p: &Project, value: serde_json::Value) -> Result<Self, String> {
|
||||
match serde_json::from_value::<Saved>(value).map_err(|e| e.to_string())? {
|
||||
Saved::Semantic(value) => crate::semantic_review::State::from_recovery(p, value)
|
||||
.map(|state| Self::Semantic(Box::new(state))),
|
||||
Saved::Ports(value) => crate::port_workflow::State::from_recovery(p, value)
|
||||
.map(|state| Self::Ports(Box::new(state))),
|
||||
Saved::Relation {
|
||||
base,
|
||||
id,
|
||||
name,
|
||||
kind,
|
||||
source,
|
||||
target,
|
||||
condition,
|
||||
layer,
|
||||
items,
|
||||
source_port,
|
||||
target_port,
|
||||
oriented,
|
||||
} => {
|
||||
check(p, &base)?;
|
||||
if items.len() > crate::source_workspace::LIMITS.model.max_records {
|
||||
return Err("Recovered relationship exceeds item limits.".into());
|
||||
}
|
||||
let mut d = Relationship {
|
||||
base_fingerprint: base,
|
||||
id,
|
||||
name,
|
||||
kind,
|
||||
source: source.map(|id| choice(p, id)),
|
||||
target: target.map(|id| choice(p, id)),
|
||||
condition: text_editor::Content::with_text(&condition),
|
||||
layer,
|
||||
items,
|
||||
source_port: endpoint(p, source_port),
|
||||
target_port: endpoint(p, target_port),
|
||||
oriented,
|
||||
sources: vec![],
|
||||
targets: vec![],
|
||||
};
|
||||
d.refresh_choices(p);
|
||||
Ok(Self::Relation(Box::new(d)))
|
||||
}
|
||||
Saved::ManualTrace {
|
||||
base,
|
||||
name,
|
||||
source,
|
||||
target,
|
||||
} => {
|
||||
check(p, &base)?;
|
||||
let mut workflow = Self::manual_trace(p, source)?;
|
||||
if let Self::ManualTrace {
|
||||
name: n, target: t, ..
|
||||
} = &mut workflow
|
||||
{
|
||||
*n = name;
|
||||
*t = target.map(|id| choice(p, id));
|
||||
}
|
||||
Ok(workflow)
|
||||
}
|
||||
Saved::Transition { base, request } => {
|
||||
check(p, &base)?;
|
||||
let plan = plan_transition(p, &request).map_err(|e| e.to_string())?;
|
||||
Ok(Self::Transition(Box::new(Transition { request, plan })))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn incomplete_relationship_and_trace_recover_raw_fields_and_reject_changed_model() {
|
||||
let mut p = Project::empty();
|
||||
let a = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Source", None)
|
||||
.unwrap();
|
||||
let mut relation = Workflow::relationship(&p, a, Layer::Oa);
|
||||
if let Workflow::Relation(d) = &mut relation {
|
||||
d.name = " unfinished name ".into();
|
||||
d.condition = text_editor::Content::with_text("raw (\n unfinished");
|
||||
d.target = None;
|
||||
}
|
||||
let saved = relation.recovery_snapshot().unwrap();
|
||||
let restored = Workflow::from_recovery(&p, saved.clone()).unwrap();
|
||||
assert_eq!(restored.recovery_snapshot().unwrap(), saved);
|
||||
let trace = Workflow::manual_trace(&p, a).unwrap();
|
||||
let saved_trace = trace.recovery_snapshot().unwrap();
|
||||
assert_eq!(
|
||||
Workflow::from_recovery(&p, saved_trace.clone())
|
||||
.unwrap()
|
||||
.recovery_snapshot()
|
||||
.unwrap(),
|
||||
saved_trace
|
||||
);
|
||||
p.name.push('!');
|
||||
assert!(Workflow::from_recovery(&p, saved).is_err());
|
||||
assert!(Workflow::from_recovery(&p, saved_trace).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn transition_recovery_replans_typed_request_without_persisted_commands() {
|
||||
let mut p = Project::empty();
|
||||
let a = p
|
||||
.add_element(Kind::Activity, Layer::Oa, "Source", None)
|
||||
.unwrap();
|
||||
let workflow = Workflow::transition(&p, a).unwrap();
|
||||
let saved = workflow.recovery_snapshot().unwrap();
|
||||
assert!(!serde_json::to_string(&saved).unwrap().contains("commands"));
|
||||
let Workflow::Transition(restored) = Workflow::from_recovery(&p, saved).unwrap() else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(restored.plan.base_fingerprint, project_fingerprint(&p));
|
||||
assert_eq!(restored.request.sources, vec![a]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user