735 lines
25 KiB
Rust
735 lines
25 KiB
Rust
//! Transport evidence for the published R6 contract, not a second model fixture.
|
|
use super::*;
|
|
use oa_core::source_project::{ProjectLimits, SourceBundle, compile_bundle};
|
|
use oa_core::{DataTypeDefinition, DataValue, SharedBody, TransmissionProtocol};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn documented_data() -> Project {
|
|
let contract = include_str!("../../../../docs/language.rst");
|
|
let example = contract
|
|
.split("This complete data contract example is also exercised through the REST API:")
|
|
.nth(1)
|
|
.unwrap()
|
|
.split(".. code-block:: text")
|
|
.nth(1)
|
|
.unwrap();
|
|
let source = example
|
|
.lines()
|
|
.skip_while(|line| line.trim().is_empty())
|
|
.take_while(|line| line.starts_with(" ") || line.is_empty())
|
|
.map(|line| line.strip_prefix(" ").unwrap_or(line))
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
let model = oa_core::language::parse(&source).unwrap();
|
|
oa_core::validate_candidate(&model).unwrap();
|
|
model
|
|
}
|
|
fn id(model: &Project, alias: &str) -> Id {
|
|
model
|
|
.identities()
|
|
.into_iter()
|
|
.find(|(_, a)| *a == alias)
|
|
.unwrap()
|
|
.0
|
|
}
|
|
fn assert_pair(stored: &StoredProject) {
|
|
let compiled = compile_bundle(&stored.source_project, ProjectLimits::default()).unwrap();
|
|
assert_eq!(compiled.decoded.project, stored.model);
|
|
assert_eq!(compiled.bundle, stored.source_project);
|
|
}
|
|
async fn commands(
|
|
server: &TestServer,
|
|
current: &StoredProject,
|
|
commands: Vec<Command>,
|
|
) -> reqwest::Response {
|
|
server
|
|
.client
|
|
.post(server.url(&format!("/projects/{}/transactions", current.resource_id)))
|
|
.header("if-match", current.etag())
|
|
.json(&json!({"commands":commands}))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn published_data_contract_queries_atomic_edits_identity_and_restart() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let model = documented_data();
|
|
let (resource, _) = server.import(&model).await;
|
|
let before = server.get(resource).await;
|
|
assert_eq!(before.model, model);
|
|
assert_pair(&before);
|
|
for (kind, count) in [("InterfaceItemAllocation", 2), ("DataAssociation", 1)] {
|
|
let result: Value = server
|
|
.client
|
|
.get(server.url(&format!("/projects/{resource}/relations?kind={kind}")))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.error_for_status()
|
|
.unwrap()
|
|
.json()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(result["items"].as_array().unwrap().len(), count);
|
|
}
|
|
let voltage = id(&model, "voltage");
|
|
let mut invalid = model.elements[&voltage].clone();
|
|
let Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Real { maximum, .. },
|
|
..
|
|
}) = invalid.shared_mut()
|
|
else {
|
|
panic!()
|
|
};
|
|
*maximum = Some(12.0); // Existing default24V is outside this bound.
|
|
let filename = directory.path().join(format!("{resource}.json"));
|
|
let disk_before = std::fs::read(&filename).unwrap();
|
|
assert_eq!(
|
|
commands(
|
|
&server,
|
|
&before,
|
|
vec![Command::EditElement(voltage, invalid.clone())]
|
|
)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNPROCESSABLE_ENTITY
|
|
);
|
|
assert_eq!(server.get(resource).await.etag(), before.etag());
|
|
assert_eq!(std::fs::read(&filename).unwrap(), disk_before);
|
|
|
|
let Some(SharedBody::DataType { default, .. }) = invalid.shared_mut() else {
|
|
panic!()
|
|
};
|
|
*default = Some(DataValue::Real(12.0));
|
|
let nav = id(&model, "nav_operation");
|
|
let other = id(&model, "monitor_operation");
|
|
let mut allocation = model.relations[&nav].clone();
|
|
let oa_core::RelationBody::InterfaceItemAllocation { transmission, .. } = &mut allocation.body
|
|
else {
|
|
panic!()
|
|
};
|
|
*transmission = TransmissionProtocol::Unicast;
|
|
let artifact = oa_core::ArtifactLink {
|
|
id: Id::new_v4(),
|
|
alias: "navigation_evidence".into(),
|
|
name: "Navigation CAD evidence".into(),
|
|
description: "Independent allocation evidence".into(),
|
|
owner: nav,
|
|
relation: oa_core::ArtifactRelation::Verifies,
|
|
uri: "cad://navigation/connector".into(),
|
|
revision: Some("review-17".into()),
|
|
fragment: Some("pin-3".into()),
|
|
};
|
|
let response = commands(
|
|
&server,
|
|
&before,
|
|
vec![
|
|
Command::EditElement(voltage, invalid),
|
|
Command::EditRelation(nav, allocation.clone()),
|
|
Command::CreateArtifact(artifact.clone()),
|
|
],
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::OK,
|
|
"{}",
|
|
response.text().await.unwrap()
|
|
);
|
|
let changed = server.get(resource).await;
|
|
assert_eq!(changed.revision, before.revision + 1);
|
|
assert_eq!(changed.model.relations[&other], model.relations[&other]);
|
|
assert_eq!(changed.model.relations[&nav], allocation);
|
|
assert_eq!(changed.model.artifact_links[&artifact.id], artifact);
|
|
assert_pair(&changed);
|
|
assert_eq!(
|
|
commands(&server, &before, vec![Command::Rename(nav, "Stale".into())])
|
|
.await
|
|
.status(),
|
|
StatusCode::PRECONDITION_FAILED
|
|
);
|
|
|
|
let renamed = Id::new_v4();
|
|
assert_eq!(
|
|
commands(&server, &changed, vec![Command::Reidentify(nav, renamed)])
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
let reidentified = server.get(resource).await;
|
|
assert_eq!(
|
|
reidentified.model.artifact_links[&artifact.id].owner,
|
|
renamed
|
|
);
|
|
// This allocation is not enforcing a live boundary, so removal is valid.
|
|
assert_eq!(
|
|
commands(&server, &reidentified, vec![Command::Delete(renamed)])
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
let removed = server.get(resource).await;
|
|
assert!(removed.model.elements.contains_key(&id(&model, "navigate")));
|
|
assert_eq!(removed.model.relations[&other], model.relations[&other]);
|
|
assert!(!removed.model.artifact_links.contains_key(&artifact.id));
|
|
assert_pair(&removed);
|
|
let source = server
|
|
.client
|
|
.get(server.url(&format!("/projects/{resource}/source")))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap();
|
|
assert!(source.starts_with("arcadia 4;"));
|
|
assert_eq!(oa_core::language::parse(&source).unwrap(), removed.model);
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let reopened = server.get(resource).await;
|
|
assert_eq!(reopened.model, removed.model);
|
|
assert_eq!(reopened.etag(), removed.etag());
|
|
assert_pair(&reopened);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn authored_schema3_bundle_upgrade_reports_data_defaults_and_preserves_files() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let old = SourceBundle {
|
|
entry: "legacy.arcadia".into(), files: BTreeMap::from([
|
|
("legacy.arcadia".into(), "// Entry rationale\narcadia 3; project old_data \"Old data\" { include \"dictionary.arcadia\"; }\n".into()),
|
|
("dictionary.arcadia".into(), "// Event rationale\nsa_exchange_item notification \"Notification\" { kind Event; }\n// Interface rationale\nsa_interface contract \"Contract\" { items [@notification]; }\n".into())
|
|
])
|
|
};
|
|
let response = server
|
|
.client
|
|
.post(server.url("/projects/import-source-project"))
|
|
.json(&old)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::CREATED);
|
|
let summary: Value = response.json().await.unwrap();
|
|
let resource = summary["resource_id"].as_str().unwrap().parse().unwrap();
|
|
let stored = server.get(resource).await;
|
|
let report = stored.migration.as_ref().unwrap();
|
|
assert_eq!((report.from_schema, report.to_schema), (3, 4));
|
|
assert_eq!(report.generated_interface_allocations.len(), 1);
|
|
assert!(!report.semantic_defaults.is_empty());
|
|
let validated: Value = server
|
|
.client
|
|
.post(server.url(&format!("/projects/{resource}/validation")))
|
|
.json(&json!({"format":"source-project","source_project":old}))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.json()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(validated["valid"], true);
|
|
assert_eq!(validated["migration"]["from_schema"], 3);
|
|
assert_eq!(
|
|
validated["migration"]["generated_interface_allocations"]
|
|
.as_array()
|
|
.unwrap()
|
|
.len(),
|
|
1
|
|
);
|
|
assert_eq!(stored.source_project.files.len(), 2);
|
|
assert!(stored.source_project.files["legacy.arcadia"].contains("arcadia 4;"));
|
|
for text in [
|
|
"// Event rationale",
|
|
"// Interface rationale",
|
|
"interface_item_allocation",
|
|
] {
|
|
assert!(stored.source_project.files["dictionary.arcadia"].contains(text));
|
|
}
|
|
assert_pair(&stored);
|
|
let filename = directory.path().join(format!("{resource}.json"));
|
|
let bytes = std::fs::read(&filename).unwrap();
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let reopened = server.get(resource).await;
|
|
assert_eq!(reopened.model, stored.model);
|
|
assert_eq!(reopened.source_project, stored.source_project);
|
|
assert!(reopened.migration.is_none());
|
|
assert_eq!(std::fs::read(filename).unwrap(), bytes);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn existing_schema3_coupled_envelope_upgrades_in_memory_until_explicit_save() {
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let resource = Id::new_v4();
|
|
let filename = directory.path().join(format!("{resource}.json"));
|
|
let old_model: Value = serde_json::from_str(include_str!(
|
|
"../../../oa-core/tests/fixtures/v3-data-contract.json"
|
|
))
|
|
.unwrap();
|
|
let old_bundle = SourceBundle {
|
|
entry: "project.arcadia".into(),
|
|
files: BTreeMap::from([(
|
|
"project.arcadia".into(),
|
|
include_str!("../../../oa-core/tests/fixtures/v3-data-contract.arcadia").into(),
|
|
)]),
|
|
};
|
|
let original = serde_json::to_vec(&json!({"resource_id":resource,"revision":17,"model":old_model,"source_project":old_bundle})).unwrap();
|
|
std::fs::write(&filename, &original).unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let opened = server.get(resource).await;
|
|
assert_eq!(opened.revision, 17);
|
|
assert_eq!(opened.model.schema_version, 4);
|
|
assert_eq!(opened.migration.as_ref().unwrap().from_schema, 3);
|
|
assert!(opened.source_project.files["project.arcadia"].contains("arcadia 4;"));
|
|
assert_pair(&opened);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), original);
|
|
assert_eq!(
|
|
commands(
|
|
&server,
|
|
&opened,
|
|
vec![Command::Rename(
|
|
opened.model.id,
|
|
"Reviewed data migration".into()
|
|
)]
|
|
)
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
let saved = server.get(resource).await;
|
|
assert_eq!(saved.revision, 18);
|
|
let bytes = std::fs::read(&filename).unwrap();
|
|
assert_ne!(bytes, original);
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let reopened = server.get(resource).await;
|
|
assert_eq!(reopened.model, saved.model);
|
|
assert_eq!(reopened.source_project, saved.source_project);
|
|
assert_eq!(reopened.etag(), saved.etag());
|
|
assert!(reopened.migration.is_none());
|
|
assert_eq!(std::fs::read(&filename).unwrap(), bytes);
|
|
server.stop().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn qualified_call_reply_values_reject_invalid_edits_and_survive_identity_source_and_restart()
|
|
{
|
|
use oa_core::{
|
|
Element, FieldStorage, MessageKind, MessageValue, OccurrenceBody, ParameterDirection,
|
|
PortContract, RelationBody, ScenarioViewpoint, TypeRef,
|
|
};
|
|
fn values(model: &Project, occurrence: Id) -> &[MessageValue] {
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = model.elements[&occurrence].shared()
|
|
else {
|
|
panic!("Expected message");
|
|
};
|
|
values
|
|
}
|
|
fn edited_values(model: &Project, occurrence: Id, next: Vec<MessageValue>) -> Element {
|
|
let mut element = model.elements[&occurrence].clone();
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { values, .. },
|
|
..
|
|
}) = element.shared_mut()
|
|
else {
|
|
panic!("Expected message");
|
|
};
|
|
*values = next;
|
|
element
|
|
}
|
|
let mut model = documented_data();
|
|
let operation = id(&model, "navigate");
|
|
let coordinate = id(&model, "coordinate");
|
|
let located = id(&model, "located");
|
|
let latitude = id(&model, "latitude");
|
|
let origin = id(&model, "origin");
|
|
let destination = id(&model, "destination");
|
|
let accepted = id(&model, "accepted");
|
|
// The second parameter inherits the very same latitude identity.
|
|
let Some(SharedBody::DataField { type_ref, .. }) =
|
|
model.elements.get_mut(&destination).unwrap().shared_mut()
|
|
else {
|
|
panic!();
|
|
};
|
|
*type_ref = TypeRef::Named(located);
|
|
let next = model
|
|
.add_element(
|
|
Kind::DataField,
|
|
Layer::Sa,
|
|
"Next coordinate",
|
|
Some(coordinate),
|
|
)
|
|
.unwrap();
|
|
*model.elements.get_mut(&next).unwrap().shared_mut().unwrap() = SharedBody::DataField {
|
|
type_ref: TypeRef::Named(coordinate),
|
|
minimum: 0,
|
|
maximum: Some(1),
|
|
direction: ParameterDirection::Unspecified,
|
|
storage: FieldStorage::Value,
|
|
unit: String::new(),
|
|
default: None,
|
|
};
|
|
let requester = model
|
|
.add_element(Kind::Function, Layer::Sa, "Request navigation", None)
|
|
.unwrap();
|
|
let provider = model
|
|
.add_element(Kind::Function, Layer::Sa, "Provide navigation", None)
|
|
.unwrap();
|
|
let output = Id::new_v4();
|
|
let input = Id::new_v4();
|
|
for (port, contract) in [
|
|
(
|
|
output,
|
|
PortContract::FunctionOutput {
|
|
outgoing_items: vec![operation],
|
|
},
|
|
),
|
|
(
|
|
input,
|
|
PortContract::FunctionInput {
|
|
incoming_items: vec![operation],
|
|
},
|
|
),
|
|
] {
|
|
let (owner, name) = if port == output {
|
|
(requester, "Navigation output")
|
|
} else {
|
|
(provider, "Navigation input")
|
|
};
|
|
let element = oa_core::ports::make_port(&model, port, owner, name, contract).unwrap();
|
|
model.elements.insert(port, element);
|
|
}
|
|
let exchange = model.relate(
|
|
RelationKind::FunctionalExchange,
|
|
"Navigation request",
|
|
requester,
|
|
provider,
|
|
);
|
|
model.relations.get_mut(&exchange).unwrap().body = RelationBody::FunctionalExchange {
|
|
source: requester,
|
|
target: provider,
|
|
source_port: output,
|
|
target_port: input,
|
|
items: vec![operation],
|
|
};
|
|
let scenario = model
|
|
.add_element(Kind::Scenario, Layer::Sa, "Navigation conversation", None)
|
|
.unwrap();
|
|
let Some(SharedBody::Scenario { viewpoint, .. }) =
|
|
model.elements.get_mut(&scenario).unwrap().shared_mut()
|
|
else {
|
|
panic!();
|
|
};
|
|
*viewpoint = ScenarioViewpoint::Functional;
|
|
let sender = model
|
|
.add_element(Kind::Lifeline, Layer::Sa, "Requester line", Some(scenario))
|
|
.unwrap();
|
|
let receiver = model
|
|
.add_element(Kind::Lifeline, Layer::Sa, "Provider line", Some(scenario))
|
|
.unwrap();
|
|
for (line, represented) in [(sender, requester), (receiver, provider)] {
|
|
*model.elements.get_mut(&line).unwrap().shared_mut().unwrap() = SharedBody::Lifeline {
|
|
represented: Some(represented),
|
|
};
|
|
}
|
|
let call = model
|
|
.add_element(
|
|
Kind::Occurrence,
|
|
Layer::Sa,
|
|
"Navigation call",
|
|
Some(scenario),
|
|
)
|
|
.unwrap();
|
|
let reply = model
|
|
.add_element(
|
|
Kind::Occurrence,
|
|
Layer::Sa,
|
|
"Navigation reply",
|
|
Some(scenario),
|
|
)
|
|
.unwrap();
|
|
for (occurrence, message_kind, from, to, reply_to) in [
|
|
(call, MessageKind::Call, sender, receiver, None),
|
|
(reply, MessageKind::Reply, receiver, sender, Some(call)),
|
|
] {
|
|
*model
|
|
.elements
|
|
.get_mut(&occurrence)
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange {
|
|
exchange: Some(exchange),
|
|
sender: Some(from),
|
|
receiver: Some(to),
|
|
message_kind,
|
|
reply_to,
|
|
values: vec![],
|
|
},
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
}
|
|
model
|
|
.elements
|
|
.get_mut(&scenario)
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.extend([call, reply]);
|
|
oa_core::validate_candidate(&model).unwrap();
|
|
|
|
let directory = tempfile::tempdir().unwrap();
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let (resource, _) = server.import(&model).await;
|
|
let before = server.get(resource).await;
|
|
let call_values = vec![
|
|
MessageValue {
|
|
path: vec![origin],
|
|
field: latitude,
|
|
expression: " origin.latitude\n+ 0.25 ".into(),
|
|
},
|
|
MessageValue {
|
|
path: vec![destination],
|
|
field: latitude,
|
|
expression: "destination.latitude".into(),
|
|
},
|
|
MessageValue {
|
|
path: vec![origin, next],
|
|
field: latitude,
|
|
expression: "previous.latitude".into(),
|
|
},
|
|
];
|
|
let reply_values = vec![MessageValue {
|
|
path: vec![],
|
|
field: accepted,
|
|
expression: "true".into(),
|
|
}];
|
|
let response = commands(
|
|
&server,
|
|
&before,
|
|
vec![
|
|
Command::EditElement(call, edited_values(&model, call, call_values.clone())),
|
|
Command::EditElement(reply, edited_values(&model, reply, reply_values.clone())),
|
|
],
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::OK,
|
|
"{}",
|
|
response.text().await.unwrap()
|
|
);
|
|
let authored = server.get(resource).await;
|
|
assert_eq!(authored.revision, before.revision + 1);
|
|
assert_eq!(values(&authored.model, call), call_values);
|
|
assert_eq!(values(&authored.model, reply), reply_values);
|
|
assert_eq!(
|
|
authored.model.elements[&latitude],
|
|
model.elements[&latitude]
|
|
);
|
|
assert_pair(&authored);
|
|
let filename = directory.path().join(format!("{resource}.json"));
|
|
let bytes = std::fs::read(&filename).unwrap();
|
|
for (occurrence, value, code) in [
|
|
(
|
|
call,
|
|
MessageValue {
|
|
path: vec![],
|
|
field: accepted,
|
|
expression: "wrong call phase".into(),
|
|
},
|
|
"message_parameter_phase",
|
|
),
|
|
(
|
|
reply,
|
|
MessageValue {
|
|
path: vec![origin],
|
|
field: latitude,
|
|
expression: "wrong reply phase".into(),
|
|
},
|
|
"message_parameter_phase",
|
|
),
|
|
(
|
|
call,
|
|
MessageValue {
|
|
path: vec![origin, accepted],
|
|
field: latitude,
|
|
expression: "unrelated hop".into(),
|
|
},
|
|
"message_value_field",
|
|
),
|
|
] {
|
|
let invalid = edited_values(&authored.model, occurrence, vec![value]);
|
|
let mut invalid_model = authored.model.clone();
|
|
invalid_model.elements.insert(occurrence, invalid.clone());
|
|
let response = commands(
|
|
&server,
|
|
&authored,
|
|
vec![Command::EditElement(occurrence, invalid)],
|
|
)
|
|
.await;
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
let problem: Value = response.json().await.unwrap();
|
|
assert_eq!(problem["code"], "command_rejected", "{problem}");
|
|
assert!(
|
|
problem["detail"].as_str().unwrap().starts_with(code),
|
|
"{problem}"
|
|
);
|
|
// Transactions report the first rejection; validation supplies located issues.
|
|
let validation: Value = server
|
|
.client
|
|
.post(server.url(&format!("/projects/{resource}/validation")))
|
|
.json(&json!({"format":"json", "model":invalid_model}))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.error_for_status()
|
|
.unwrap()
|
|
.json()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(validation["valid"], false);
|
|
assert!(
|
|
validation["issues"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|issue| issue["code"] == code
|
|
&& issue["object_id"] == json!(occurrence)
|
|
&& issue["field"] == "values.path"),
|
|
"{problem}"
|
|
);
|
|
let unchanged = server.get(resource).await;
|
|
assert_eq!(unchanged.etag(), authored.etag());
|
|
assert_eq!(unchanged.model, authored.model);
|
|
assert_eq!(unchanged.source_project, authored.source_project);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), bytes);
|
|
}
|
|
|
|
// Source transport changes one argument without conflating the shared leaf.
|
|
let source = server
|
|
.client
|
|
.get(server.url(&format!("/projects/{resource}/source")))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.error_for_status()
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap();
|
|
let edited_source = source.replace(
|
|
"\"destination.latitude\"",
|
|
"\"destination.corrected_latitude\"",
|
|
);
|
|
assert_ne!(edited_source, source);
|
|
let response = server
|
|
.client
|
|
.put(server.url(&format!("/projects/{resource}/source")))
|
|
.header("if-match", authored.etag())
|
|
.header("content-type", "text/plain")
|
|
.body(edited_source)
|
|
.send()
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::OK,
|
|
"{}",
|
|
response.text().await.unwrap()
|
|
);
|
|
let source_edited = server.get(resource).await;
|
|
assert_eq!(source_edited.revision, authored.revision + 1);
|
|
let mut expected_call = call_values.clone();
|
|
expected_call[1].expression = "destination.corrected_latitude".into();
|
|
assert_eq!(values(&source_edited.model, call), expected_call);
|
|
assert_eq!(values(&source_edited.model, reply), reply_values);
|
|
assert_pair(&source_edited);
|
|
|
|
// Reidentify every kind of enclosing hop, the shared terminal, and its base type.
|
|
let mapping = [origin, destination, next, latitude, coordinate]
|
|
.into_iter()
|
|
.map(|old| (old, Id::new_v4()))
|
|
.collect::<BTreeMap<_, _>>();
|
|
let response = commands(
|
|
&server,
|
|
&source_edited,
|
|
mapping
|
|
.iter()
|
|
.map(|(old, new)| Command::Reidentify(*old, *new))
|
|
.collect(),
|
|
)
|
|
.await;
|
|
assert_eq!(
|
|
response.status(),
|
|
StatusCode::OK,
|
|
"{}",
|
|
response.text().await.unwrap()
|
|
);
|
|
let changed = server.get(resource).await;
|
|
assert_eq!(changed.revision, source_edited.revision + 1);
|
|
for value in &mut expected_call {
|
|
value.path = value.path.iter().map(|hop| mapping[hop]).collect();
|
|
value.field = mapping[&value.field];
|
|
}
|
|
assert_eq!(values(&changed.model, call), expected_call);
|
|
assert_eq!(values(&changed.model, reply), reply_values);
|
|
assert_eq!(
|
|
changed.model.elements[&mapping[&latitude]].parent,
|
|
Some(mapping[&coordinate])
|
|
);
|
|
assert!(
|
|
matches!(changed.model.elements[&located].shared(), Some(SharedBody::DataType {
|
|
definition: DataTypeDefinition::Record { supertype: Some(base) }, ..
|
|
}) if *base == mapping[&coordinate])
|
|
);
|
|
assert!(
|
|
oa_core::data::effective_fields(&changed.model, located)
|
|
.iter()
|
|
.any(|field| field.id == mapping[&latitude])
|
|
);
|
|
for old in mapping.keys() {
|
|
assert!(!changed.model.contains(*old));
|
|
}
|
|
assert!(
|
|
matches!(changed.model.elements[&reply].shared(), Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::Exchange { reply_to: Some(correlation), .. }, ..
|
|
}) if *correlation == call)
|
|
);
|
|
assert_pair(&changed);
|
|
let source = server
|
|
.client
|
|
.get(server.url(&format!("/projects/{resource}/source")))
|
|
.send()
|
|
.await
|
|
.unwrap()
|
|
.error_for_status()
|
|
.unwrap()
|
|
.text()
|
|
.await
|
|
.unwrap();
|
|
assert!(source.starts_with("arcadia 4;"));
|
|
assert_eq!(oa_core::language::parse(&source).unwrap(), changed.model);
|
|
let saved = std::fs::read(&filename).unwrap();
|
|
server.stop().await;
|
|
let server = TestServer::start(directory.path(), None).await;
|
|
let reopened = server.get(resource).await;
|
|
assert_eq!(reopened.etag(), changed.etag());
|
|
assert_eq!(reopened.model, changed.model);
|
|
assert_eq!(reopened.source_project, changed.source_project);
|
|
assert_eq!(values(&reopened.model, call), expected_call);
|
|
assert_eq!(values(&reopened.model, reply), reply_values);
|
|
assert_pair(&reopened);
|
|
assert_eq!(std::fs::read(&filename).unwrap(), saved);
|
|
server.stop().await;
|
|
}
|