blob: 3e824bab2ddce4df1db34d05239c40a5e01bf14a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
#[path = "reconciliation.rs"]
mod reconciliation;
use super::Application;
use super::{support, timer};
use maka_event_log::{
EventLog,
root::{RootNamespaces, RootOwner},
};
use maka_plugin_core::composition::PresentationTarget;
use maka_ui_runtime::{NativeCapability, NativeExport, NativeRegistry};
use serde_json::json;
use std::{cell::Cell, collections::BTreeMap, rc::Rc, sync::Arc, time::Duration};
use uuid::Uuid;
fn namespaces(dir: &std::path::Path) -> RootNamespaces {
RootNamespaces {
ownership: dir.join("owners"),
control: dir.join("control"),
}
}
fn store(timer: &tokio::runtime::Runtime, dir: &std::path::Path) -> Arc<EventLog> {
let owner = Arc::new(RootOwner::create(&dir.join("root"), &namespaces(dir)).unwrap());
Arc::new(timer.block_on(EventLog::for_root(owner)).unwrap())
}
fn start(timer: &tokio::runtime::Runtime, store: Arc<EventLog>, id: Uuid) -> Application {
let source = timer
.block_on(store.load_application(id))
.unwrap()
.unwrap()
.into_parts()
.0
.reference;
timer
.block_on(Application::start_from_store(
store,
source,
PresentationTarget::Tui,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
crate::support::scene::registry(PresentationTarget::Tui),
vec![],
))
.unwrap()
}
#[test]
fn persisted_execution_keeps_original_bytes_source_and_root_until_mount_cleanup() {
let timer = timer();
let dir = tempfile::tempdir().unwrap();
let store = store(&timer, dir.path());
let id = Uuid::new_v4();
let (input, packages) =
support::fixture("return frame({count:state.count+1,revision:state.revision+1});");
let source = timer
.block_on(store.commit_application(id, 0, input, vec![], packages))
.unwrap();
let mut app = start(&timer, store.clone(), id);
let mount = app.mount_launch(json!({"count":3})).unwrap();
let (input, packages) =
support::fixture("return frame({count:state.count+100,revision:state.revision+1});");
let desired = timer
.block_on(store.commit_application(id, 1, input, vec![], packages))
.unwrap();
assert_ne!(source, desired);
assert_eq!(app.source(), Some(&source));
app.dispatch(&mount, 0, "inc", json!({})).unwrap();
assert_eq!(app.frame(&mount).unwrap().state["count"], 4);
drop(store);
let root = dir.path().join("root");
let ns = namespaces(dir.path());
assert_eq!(
RootOwner::open(&root, &ns).err().unwrap().kind(),
std::io::ErrorKind::WouldBlock
);
assert!(app.shutdown().is_err());
assert!(app.is_retiring(&mount));
assert_eq!(app.source(), Some(&source));
assert!(RootOwner::open(&root, &ns).is_err());
app.close(&mount, Ok(())).unwrap();
app.shutdown().unwrap();
assert_eq!(app.source(), Some(&source));
// UI acknowledgement is separate from the independent SQL owner's close.
let reopened = timer
.block_on(async {
tokio::time::timeout(Duration::from_secs(10), async {
loop {
match RootOwner::open(&root, &ns) {
Ok(owner) => break owner,
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
tokio::task::yield_now().await
}
Err(e) => panic!("{e}"),
}
}
})
.await
})
.unwrap();
let stored = timer
.block_on(EventLog::for_root(Arc::new(reopened)))
.unwrap();
assert_eq!(
timer
.block_on(stored.load_application(id))
.unwrap()
.unwrap()
.source()
.reference,
desired
);
timer.block_on(stored.close()).unwrap();
}
#[test]
fn failed_native_close_keeps_persisted_root_custody_until_confirmed_recovery() {
let timer = timer();
let dir = tempfile::tempdir().unwrap();
let store = store(&timer, dir.path());
let id = Uuid::new_v4();
let (input, packages) = support::fixture("");
timer
.block_on(store.commit_application(id, 0, input, vec![], packages))
.unwrap();
let mut app = start(&timer, store, id);
let mount = app.mount_launch(json!({})).unwrap();
app.retire(&mount).unwrap();
assert!(
app.close(&mount, Err("platform close unknown".into()))
.is_err()
);
assert!(app.shutdown().is_err());
assert!(app.frame(&mount).is_ok());
assert_eq!(
RootOwner::open(&dir.path().join("root"), &namespaces(dir.path()))
.err()
.unwrap()
.kind(),
std::io::ErrorKind::WouldBlock
);
app.close(&mount, Ok(())).unwrap();
app.shutdown().unwrap();
}
#[test]
fn missing_or_wrong_selected_presentation_never_activates_native_code() {
let timer = timer();
let dir = tempfile::tempdir().unwrap();
let store = store(&timer, dir.path());
let id = Uuid::new_v4();
let (input, packages) = support::fixture("");
let source = timer
.block_on(store.commit_application(id, 0, input, vec![], packages))
.unwrap();
let (mut host_input, packages) = support::fixture("");
host_input.environment = maka_plugin_core::composition::Environment {
location: maka_plugin_core::composition::ExecutionLocation::Host,
presentation: None,
};
host_input.instances.retain(|name, _| name == "surface");
host_input.launch = None;
let surface = packages
.into_iter()
.find(|package| package.manifest().id == "native.surface")
.unwrap();
let mut declaration = surface
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone();
let entry = declaration
.entries
.remove(&maka_plugin_core::package::EntryTarget::Tui)
.unwrap();
declaration
.entries
.insert(maka_plugin_core::package::EntryTarget::Host, entry);
let host_surface = support::package("native.surface", declaration, None);
host_input.instances.get_mut("surface").unwrap().package =
host_surface.application_ref().into();
let host_source = timer
.block_on(store.commit_application(
Uuid::new_v4(),
0,
host_input,
vec![],
vec![host_surface],
))
.unwrap();
let calls = Rc::new(Cell::new(0));
for selected in [
maka_plugin_core::deployment::DeploymentRef {
deployment: Uuid::new_v4(),
..source
},
host_source,
] {
let observed = calls.clone();
let mut native = NativeRegistry::<()>::default()
.with_scene_compiler(
support::scene::language(PresentationTarget::Tui),
support::scene::prepare,
support::scene::finish::<()>,
)
.unwrap();
native
.register("maka.ui.surface.tui", move |_, _values| {
observed.set(observed.get() + 1);
Ok(BTreeMap::from([(
"Surface".into(),
NativeExport {
contract: maka_ui_runtime::surface_contract(PresentationTarget::Tui),
value: NativeCapability::Surface(PresentationTarget::Tui),
},
)]))
})
.unwrap();
assert!(
timer
.block_on(Application::start_from_store(
store.clone(),
selected,
PresentationTarget::Tui,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
vec![]
))
.is_err()
);
assert_eq!(calls.get(), 0);
}
timer
.block_on(Arc::try_unwrap(store).ok().unwrap().close())
.unwrap();
}
#[test]
fn unavailable_native_implementation_is_rejected_before_an_earlier_factory_executes() {
use maka_plugin_core::package::{EntryTarget, Implementation};
let timer = timer();
let dir = tempfile::tempdir().unwrap();
let store = store(&timer, dir.path());
let id = Uuid::new_v4();
let (mut input, mut packages) = support::fixture("");
let mut declaration = packages[0]
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone();
declaration
.entries
.get_mut(&EntryTarget::Tui)
.unwrap()
.implementation = Implementation::Native {
implementation: "missing.surface".into(),
};
let missing = support::package("missing.surface", declaration, None);
let mut later = support::node(&missing, 100);
later.disabled = true;
input.instances.insert("z-missing".into(), later);
packages.push(missing);
// Disabled entries do not require implementation availability.
let source = timer
.block_on(store.commit_application(id, 0, input.clone(), vec![], packages.clone()))
.unwrap();
let mut app = start(&timer, store.clone(), id);
assert_eq!(app.source(), Some(&source));
app.shutdown().unwrap();
input.instances.get_mut("z-missing").unwrap().disabled = false;
let desired = timer
.block_on(store.commit_application(id, 1, input, vec![], packages))
.unwrap();
let calls = Rc::new(Cell::new(0));
let observed = calls.clone();
let mut native = NativeRegistry::<()>::default()
.with_scene_compiler(
support::scene::language(PresentationTarget::Tui),
support::scene::prepare,
support::scene::finish::<()>,
)
.unwrap();
native
.register("maka.ui.surface.tui", move |_, _values| {
observed.set(observed.get() + 1);
Ok(BTreeMap::from([(
"Surface".into(),
NativeExport {
contract: maka_ui_runtime::surface_contract(PresentationTarget::Tui),
value: NativeCapability::Surface(PresentationTarget::Tui),
},
)]))
})
.unwrap();
let result = timer.block_on(Application::start_from_store(
store.clone(),
desired,
PresentationTarget::Tui,
timer.handle().clone(),
maka_ui_js::execution(Default::default()),
native,
vec![],
));
assert!(
matches!(result, Err(maka_ui_runtime::Error::Invalid(ref text)) if text.contains("missing.surface"))
);
assert_eq!(calls.get(), 0);
timer
.block_on(Arc::try_unwrap(store).ok().unwrap().close())
.unwrap();
}
#[test]
fn bootstrap_recovery_survives_primary_activation_or_mount_failure() {
for stage in ["activation", "mount"] {
let timer = timer();
let (mut input, mut packages) =
support::fixture("return frame({...state,revision:state.revision+1});");
let index = packages
.iter()
.position(|p| p.manifest().id == "dynamic.app")
.unwrap();
let original = packages.remove(index);
let source = std::str::from_utf8(original.file("index.mjs").unwrap()).unwrap();
let needle = if stage == "activation" {
"export function activate(configuration) {"
} else {
"mount(props){"
};
assert!(source.contains(needle));
let broken = support::package(
"dynamic.app",
original
.manifest()
.application
.as_ref()
.unwrap()
.declaration()
.clone(),
Some(source.replace(
needle,
&format!("{needle}throw new Error('primary {stage} failure');"),
)),
);
input.instances.get_mut("app").unwrap().package = broken.application_ref().into();
packages.push(broken);
support::add_recovery(&mut input, &mut packages);
let dir = tempfile::tempdir().unwrap();
let store = store(&timer, dir.path());
let id = Uuid::new_v4();
let committed = timer
.block_on(store.commit_application(id, 0, input, vec![], packages))
.unwrap();
let mut app = start(&timer, store.clone(), id);
assert_eq!(
app.deployment_status().effective,
if stage == "activation" {
None
} else {
Some(committed)
}
);
let root = app.mount_bootstrap(json!({})).unwrap();
assert_eq!(
app.root_kind(&root).unwrap(),
maka_plugin_core::composition::RootKind::Recovery
);
assert!(app.deployment_status().error.is_some());
assert!(
!app.can_recover(&root),
"failure in recovery must not recursively mount another root"
);
assert!(!app.can_resume_launch(&root));
app.dispatch(&root, 0, "inc", json!({})).unwrap();
assert_eq!(app.frame(&root).unwrap().state["count"], 1);
app.retire(&root).unwrap();
for mount in app.retiring_mounts() {
app.close(&mount, Ok(())).unwrap();
}
app.shutdown().unwrap();
drop(app);
timer
.block_on(Arc::try_unwrap(store).ok().unwrap().close())
.unwrap();
}
}