Checkpoint source 7 workflows and in-progress diagram authoring redesign
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
//! Private synchronization for real-HTTP tests; absent from production builds.
|
||||
use super::{ApiError, ApiResult, Id, PreparedProject, Project, SourceBundle, Store};
|
||||
use std::{sync::mpsc, time::Duration};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub(crate) struct PreparedEvidence {
|
||||
pub model: Project,
|
||||
pub bundle: SourceBundle,
|
||||
}
|
||||
|
||||
pub(crate) struct Armed {
|
||||
id: Id,
|
||||
expected: String,
|
||||
entered: oneshot::Sender<PreparedEvidence>,
|
||||
resume: mpsc::Receiver<bool>,
|
||||
}
|
||||
|
||||
/// Dropping an unfinished test cancels the worker instead of stranding it.
|
||||
pub(crate) struct ReleaseOnDrop(Option<mpsc::Sender<bool>>);
|
||||
|
||||
impl ReleaseOnDrop {
|
||||
pub(crate) fn release(mut self) {
|
||||
self.0.take().unwrap().send(true).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ReleaseOnDrop {
|
||||
fn drop(&mut self) {
|
||||
if let Some(sender) = self.0.take() {
|
||||
let _ = sender.send(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub(crate) fn pause_next_replacement(
|
||||
&self,
|
||||
id: Id,
|
||||
expected: String,
|
||||
) -> (oneshot::Receiver<PreparedEvidence>, ReleaseOnDrop) {
|
||||
let (entered, receiver) = oneshot::channel();
|
||||
let (resume, wait) = mpsc::channel();
|
||||
let mut slot = self.preparation_gate.lock().unwrap();
|
||||
assert!(slot.is_none(), "one gate per store instance");
|
||||
*slot = Some(Armed {
|
||||
id,
|
||||
expected,
|
||||
entered,
|
||||
resume: wait,
|
||||
});
|
||||
(receiver, ReleaseOnDrop(Some(resume)))
|
||||
}
|
||||
|
||||
pub(super) fn wait_after_preparation(
|
||||
&self,
|
||||
id: Id,
|
||||
expected: &str,
|
||||
prepared: &PreparedProject,
|
||||
) -> ApiResult<()> {
|
||||
let armed = {
|
||||
let mut slot = self.preparation_gate.lock().unwrap();
|
||||
if slot
|
||||
.as_ref()
|
||||
.is_some_and(|gate| gate.id == id && gate.expected == expected)
|
||||
{
|
||||
slot.take()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
let Some(armed) = armed else {
|
||||
return Ok(());
|
||||
};
|
||||
// Neither the catalog nor the gate mutex is held while the real model
|
||||
// worker waits. Evidence comes from the already validated candidate.
|
||||
armed
|
||||
.entered
|
||||
.send(PreparedEvidence {
|
||||
model: prepared.model().clone(),
|
||||
bundle: prepared.bundle().clone(),
|
||||
})
|
||||
.map_err(|_| ApiError::internal("Preparation test observer disappeared."))?;
|
||||
match armed.resume.recv_timeout(Duration::from_secs(15)) {
|
||||
Ok(true) => Ok(()),
|
||||
_ => Err(ApiError::internal(
|
||||
"Preparation test cancelled or timed out.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn preparation_gate_drop_cancels_without_installing_candidate() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().to_owned();
|
||||
let (store, initial) = tokio::task::spawn_blocking(move || {
|
||||
let store = std::sync::Arc::new(Store::open(path).unwrap());
|
||||
let initial = store.create(Project::empty()).unwrap();
|
||||
(store, initial)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let original_bytes = std::fs::read(
|
||||
directory
|
||||
.path()
|
||||
.join(format!("{}.json", initial.resource_id)),
|
||||
)
|
||||
.unwrap();
|
||||
let (entered, release) = store.pause_next_replacement(initial.resource_id, initial.etag());
|
||||
let mut model = initial.model.clone();
|
||||
model.name = "Must not be installed".into();
|
||||
let worker_store = store.clone();
|
||||
let id = initial.resource_id;
|
||||
let etag = initial.etag();
|
||||
let worker = tokio::task::spawn_blocking(move || {
|
||||
worker_store.replace_decoded(
|
||||
id,
|
||||
&etag,
|
||||
oa_core::persistence::DecodeResult {
|
||||
project: model,
|
||||
migration: None,
|
||||
},
|
||||
)
|
||||
});
|
||||
tokio::time::timeout(Duration::from_secs(10), entered)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
drop(release);
|
||||
let error = tokio::time::timeout(Duration::from_secs(2), worker)
|
||||
.await
|
||||
.expect("dropping the test guard must release its worker")
|
||||
.unwrap()
|
||||
.unwrap_err();
|
||||
assert_eq!(error.detail, "Preparation test cancelled or timed out.");
|
||||
assert_eq!(store.get(id).unwrap().model, initial.model);
|
||||
assert_eq!(
|
||||
std::fs::read(directory.path().join(format!("{id}.json"))).unwrap(),
|
||||
original_bytes
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user