Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions crates/bitty-platform/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ gui-tests = []
# (`App::run` -> DisplayUnavailable) without requiring a window system.
# CI never enables gui-tests; the default run proves headless still works.
headless = []
# In-memory EventContext backend; never enabled by default release builds.
input-vm-test-support = []

# Runs as a plain process entry point (`harness = false`) because winit
# requires event-loop creation on the OS main thread; see the file header.
Expand Down
97 changes: 85 additions & 12 deletions crates/bitty-platform/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,72 @@ fn map_loop_error(error: EventLoopError) -> PlatformError {
/// Borrowed per callback; methods are cheap and never block.
#[derive(Debug)]
pub struct EventContext<'a> {
event_loop: &'a ActiveEventLoop,
registry: &'a mut Registry,
backend: EventContextBackend<'a>,
}

#[derive(Debug)]
enum EventContextBackend<'a> {
Live {
event_loop: &'a ActiveEventLoop,
registry: &'a mut Registry,
},
#[cfg(feature = "input-vm-test-support")]
Recording(&'a std::cell::RefCell<Vec<RecordingOperation>>),
}

/// Loop requests recorded without constructing an OS event loop or window.
#[cfg(feature = "input-vm-test-support")]
#[doc(hidden)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RecordingOperation {
/// Window creation was requested and refused with `WindowCreation`.
WindowCreationRefused,
/// Finish the event loop.
Exit,
/// Busy-poll.
Poll,
/// Wait indefinitely.
Wait,
/// Wait until the given monotonic deadline.
WaitUntil(std::time::Instant),
}

impl EventContext<'_> {
/// Constructs an in-memory context for explicitly opted-in handler tests.
/// This creates no registry, window, OS loop, proxy, or desktop sink.
#[cfg(feature = "input-vm-test-support")]
#[doc(hidden)]
pub fn recording(operations: &std::cell::RefCell<Vec<RecordingOperation>>) -> EventContext<'_> {
EventContext {
backend: EventContextBackend::Recording(operations),
}
}

/// Creates a window according to `config`.
///
/// # Errors
///
/// Returns [`PlatformError::WindowCreation`] when the platform refuses
/// the request; the upstream diagnostic text is preserved.
pub fn create_window(&mut self, config: WindowConfig) -> Result<WindowHandle, PlatformError> {
let (event_loop, registry) = match &mut self.backend {
EventContextBackend::Live {
event_loop,
registry,
} => (event_loop, registry),
#[cfg(feature = "input-vm-test-support")]
EventContextBackend::Recording(operations) => {
operations
.borrow_mut()
.push(RecordingOperation::WindowCreationRefused);
return Err(PlatformError::WindowCreation(
"recording context refuses native window creation".to_string(),
));
}
};
let blur_radius = config.blur_radius();
let attributes = config.into_attributes();
let window = self
.event_loop
let window = event_loop
.create_window(attributes)
.map_err(|error| PlatformError::WindowCreation(error.to_string()))?;

Expand All @@ -149,7 +199,7 @@ impl EventContext<'_> {
crate::blur::apply_blur(&window, blur_radius);
}

let id = self.registry.register(&window.id());
let id = registry.register(&window.id());
Ok(WindowHandle {
id,
window: Arc::new(window),
Expand All @@ -158,17 +208,23 @@ impl EventContext<'_> {

/// Requests that the event loop finish after the current iteration.
pub fn exit(&self) {
self.event_loop.exit();
match &self.backend {
EventContextBackend::Live { event_loop, .. } => event_loop.exit(),
#[cfg(feature = "input-vm-test-support")]
EventContextBackend::Recording(operations) => {
operations.borrow_mut().push(RecordingOperation::Exit);
}
}
}

/// Puts the loop into busy-polling mode (continuous redraw loops).
pub fn set_poll(&self) {
self.event_loop.set_control_flow(ControlFlow::Poll);
self.set_control_flow(ControlFlow::Poll);
}

/// Puts the loop back into energy-saving wait mode (default).
pub fn set_wait(&self) {
self.event_loop.set_control_flow(ControlFlow::Wait);
self.set_control_flow(ControlFlow::Wait);
}

/// Waits until `deadline` and then delivers
Expand All @@ -178,8 +234,23 @@ impl EventContext<'_> {
/// busy-polling — the hover dwell delay uses it so focus still applies
/// when the pointer stops moving and no further input events arrive.
pub fn set_wait_until(&self, deadline: std::time::Instant) {
self.event_loop
.set_control_flow(ControlFlow::WaitUntil(deadline));
self.set_control_flow(ControlFlow::WaitUntil(deadline));
}

fn set_control_flow(&self, control_flow: ControlFlow) {
match &self.backend {
EventContextBackend::Live { event_loop, .. } => {
event_loop.set_control_flow(control_flow)
}
#[cfg(feature = "input-vm-test-support")]
EventContextBackend::Recording(operations) => {
operations.borrow_mut().push(match control_flow {
ControlFlow::Poll => RecordingOperation::Poll,
ControlFlow::Wait => RecordingOperation::Wait,
ControlFlow::WaitUntil(deadline) => RecordingOperation::WaitUntil(deadline),
});
}
}
}
}

Expand Down Expand Up @@ -567,8 +638,10 @@ impl<H: AppHandler> Adapter<H> {
fn dispatch(&mut self, event_loop: &ActiveEventLoop, event: PlatformEvent) {
let Self { handler, registry } = self;
let mut ctx = EventContext {
event_loop,
registry,
backend: EventContextBackend::Live {
event_loop,
registry,
},
};
handler.handle_event(&mut ctx, event);
}
Expand Down
4 changes: 4 additions & 0 deletions crates/bitty-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ publish = true
[lints]
workspace = true

[features]
# Read-only VM accounting and safe construction for the explicit regression.
input-vm-test-support = []

# CTX-0906: hermetic native-component test fixture. Speaks wire protocol v1
# over stdio so the broker integration tests spawn a real coprocess without
# any network access; never installed or packaged (publish = false, and the
Expand Down
78 changes: 78 additions & 0 deletions crates/bitty-runtime/src/plugin_runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,17 @@ struct PluginEntry {

type DiscoveryResults = Vec<(PluginId, Result<(), PluginRuntimeError>)>;

/// A failed read-only owning-VM counter observation (never a zero fallback).
#[cfg(feature = "input-vm-test-support")]
#[doc(hidden)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum VmExecutionCountError {
/// No entry or no VM for this plugin.
MissingVm,
/// A mutable VM borrow is active on the owning thread.
Busy,
}

/// Orchestrates discovery, activation, and dispatch for plugin VMs.
///
/// The struct is `!Send` because it owns Phodopus VMs; callers use it on the
Expand Down Expand Up @@ -760,6 +771,24 @@ pub struct PluginRuntime {
}

impl PluginRuntime {
/// Reads actual owning-VM entries under a short, shared owning-thread borrow.
/// No VM/reference escapes and no execution or dispatch occurs.
///
/// # Errors
///
/// Distinguishes missing VM from a busy mutable borrow.
#[cfg(feature = "input-vm-test-support")]
#[doc(hidden)]
pub fn vm_total_executions(&self, id: &PluginId) -> Result<u64, VmExecutionCountError> {
let vm = self
.entries
.get(id)
.and_then(|entry| entry.vm.as_ref())
.ok_or(VmExecutionCountError::MissingVm)?;
let vm = vm.try_borrow().map_err(|_| VmExecutionCountError::Busy)?;
Ok(vm.total_executions())
}

/// Create an empty runtime.
#[must_use]
pub fn new(config: PluginRuntimeConfig) -> Self {
Expand Down Expand Up @@ -3886,6 +3915,55 @@ impl PluginRuntime {
mod tests {
use super::*;

#[cfg(feature = "input-vm-test-support")]
#[test]
fn input_vm_boundary_counter_distinguishes_missing_busy_and_zero() {
// Constructor-only control: no Lua source is evaluated or dispatched.
let mut runtime = PluginRuntime::new(PluginRuntimeConfig::default());
let manifest = manifest_toml::parse_manifest(
br#"[plugin]
id = "bitty-featured.input-counter"
name = "Input counter fixture"
version = "0.0.1"
description = "Read-only owning-VM accounting"
[compat]
plugin-api = "^1.0"
"#,
)
.expect("in-memory manifest");
let id = manifest.id().clone();
assert_eq!(
runtime.vm_total_executions(&id),
Err(VmExecutionCountError::MissingVm)
);
runtime.insert_package(
id.clone(),
PluginPackage {
manifest,
module_root: PathBuf::new(),
source_class: SourceClass::LocalPath,
unverified: true,
granted: None,
},
);
assert_eq!(
runtime.vm_total_executions(&id),
Err(VmExecutionCountError::MissingVm)
);
let vm = Rc::new(RefCell::new(
build_plugin_vm(id.as_str(), Some(VmBudgets::default())).expect("bounded pure VM"),
));
runtime.entries.get_mut(&id).expect("entry").vm = Some(vm.clone());
assert_eq!(runtime.vm_total_executions(&id), Ok(0));
let busy = vm.borrow_mut();
assert_eq!(
runtime.vm_total_executions(&id),
Err(VmExecutionCountError::Busy)
);
drop(busy);
assert_eq!(runtime.vm_total_executions(&id), Ok(0));
}

#[test]
fn runtime_error_taxonomy_codes_and_classes() {
let plugin = "xuepoo.test".to_string();
Expand Down
26 changes: 25 additions & 1 deletion crates/bitty-runtime/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1535,6 +1535,7 @@ impl Runtime {
pipeline_capacity,
side_capacity,
false,
Clipboard::new,
)
}

Expand Down Expand Up @@ -1563,6 +1564,28 @@ impl Runtime {
DEFAULT_PLUGIN_PIPELINE_CAPACITY,
DEFAULT_PLUGIN_SIDE_CAPACITY,
true,
Clipboard::new,
)
}

/// Builds only the finite input/VM regression's in-memory runtime.
/// Default configuration has no image paths or roots; deterministic glyphs,
/// software surface, and headless clipboard are selected before creation.
/// Ordinary constructors retain their original backend selection.
///
/// # Errors
///
/// Returns the same validation/render errors as [`Self::new`].
#[cfg(feature = "input-vm-test-support")]
#[doc(hidden)]
pub fn for_input_vm_regression() -> Result<Self, RuntimeError> {
Self::with_plugin_host_capacity_and_rasterizer(
RuntimeConfig::default(),
DEFAULT_PLUGIN_DROP_POLICY,
DEFAULT_PLUGIN_PIPELINE_CAPACITY,
DEFAULT_PLUGIN_SIDE_CAPACITY,
true,
Clipboard::new_headless,
)
}

Expand All @@ -1572,6 +1595,7 @@ impl Runtime {
pipeline_capacity: usize,
side_capacity: usize,
deterministic_rasterizer: bool,
make_clipboard: fn() -> Clipboard,
) -> Result<Self, RuntimeError> {
config.validate()?;
if pipeline_capacity == 0 || side_capacity == 0 {
Expand Down Expand Up @@ -1666,7 +1690,7 @@ impl Runtime {
layout,
focus,
container,
clipboard: Clipboard::new(),
clipboard: make_clipboard(),
selection_state: None,
click_tracker: ClickTracker::new(),
last_click_count: crate::runtime::click::CLICK_COUNT_MIN,
Expand Down
2 changes: 2 additions & 0 deletions crates/bitty-terminal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ default = []
# Enables `bitty dev capture|synthesize|dump|overlay` (CTX-0922): developer
# headless captures, dumps, and overlay proofs stay out of release builds.
dev-tools = []
# Explicit, default-off application boundary regression infrastructure.
input-vm-test-support = ["bitty-platform/input-vm-test-support", "bitty-runtime/input-vm-test-support"]

[dev-dependencies]
# Hermetic `ServeContext` constructors and unbound bearer minters (#1519).
Expand Down
Loading
Loading