kicad-ipc-rs/src/blocking.rs

668 lines
26 KiB
Rust

use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::mpsc::{self, SyncSender};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle, ThreadId};
use std::time::Duration;
use prost_types::Any;
use crate::client::{ClientBuilder, KiCadClient};
use crate::error::KiCadError;
use crate::model::board::*;
use crate::model::common::*;
const BLOCKING_QUEUE_CAPACITY: usize = 64;
type Job = Box<dyn FnOnce(&tokio::runtime::Runtime) + Send + 'static>;
#[derive(Debug)]
struct BlockingCore {
job_tx: Mutex<Option<SyncSender<Job>>>,
worker_thread_id: ThreadId,
worker_join: Mutex<Option<JoinHandle<()>>>,
}
impl BlockingCore {
fn start() -> Result<Arc<Self>, KiCadError> {
let (job_tx, job_rx) = mpsc::sync_channel::<Job>(BLOCKING_QUEUE_CAPACITY);
let (init_tx, init_rx) = mpsc::sync_channel::<Result<ThreadId, KiCadError>>(1);
let worker_name = format!("kicad-ipc-blocking-runtime-{}", std::process::id());
let worker_join = thread::Builder::new()
.name(worker_name)
.spawn(move || {
let runtime = match tokio::runtime::Builder::new_current_thread()
.enable_time()
.build()
{
Ok(runtime) => runtime,
Err(err) => {
let _ = init_tx.send(Err(KiCadError::RuntimeJoin(err.to_string())));
return;
}
};
let _ = init_tx.send(Ok(thread::current().id()));
for job in job_rx {
job(&runtime);
}
})
.map_err(|err| KiCadError::RuntimeJoin(err.to_string()))?;
let worker_thread_id = init_rx
.recv()
.map_err(|_| KiCadError::BlockingRuntimeClosed)??;
Ok(Arc::new(Self {
job_tx: Mutex::new(Some(job_tx)),
worker_thread_id,
worker_join: Mutex::new(Some(worker_join)),
}))
}
fn shutdown(&self) {
if let Ok(mut tx_guard) = self.job_tx.lock() {
tx_guard.take();
}
let handle = match self.worker_join.lock() {
Ok(mut guard) => guard.take(),
Err(_) => None,
};
if let Some(handle) = handle {
if thread::current().id() != self.worker_thread_id {
let _ = handle.join();
}
}
}
fn call<T, F>(&self, f: F) -> Result<T, KiCadError>
where
T: Send + 'static,
F: FnOnce(&tokio::runtime::Runtime) -> Result<T, KiCadError> + Send + 'static,
{
let sender = {
let guard = self
.job_tx
.lock()
.map_err(|_| KiCadError::BlockingRuntimeClosed)?;
guard
.as_ref()
.cloned()
.ok_or(KiCadError::BlockingRuntimeClosed)?
};
let (result_tx, result_rx) = mpsc::sync_channel::<Result<T, KiCadError>>(1);
sender
.send(Box::new(move |runtime| {
let result = f(runtime);
let _ = result_tx.send(result);
}))
.map_err(|_| KiCadError::BlockingRuntimeClosed)?;
result_rx
.recv()
.map_err(|_| KiCadError::BlockingRuntimeClosed)?
}
}
impl Drop for BlockingCore {
fn drop(&mut self) {
self.shutdown();
}
}
#[derive(Clone, Debug)]
pub struct KiCadClientBlocking {
inner: KiCadClient,
core: Arc<BlockingCore>,
}
#[derive(Clone, Debug)]
pub struct KiCadClientBlockingBuilder {
inner: ClientBuilder,
}
impl KiCadClientBlockingBuilder {
pub fn new() -> Self {
Self {
inner: ClientBuilder::new(),
}
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.inner = self.inner.timeout(timeout);
self
}
pub fn socket_path(mut self, socket_path: impl Into<String>) -> Self {
self.inner = self.inner.socket_path(socket_path);
self
}
pub fn token(mut self, token: impl Into<String>) -> Self {
self.inner = self.inner.token(token);
self
}
pub fn client_name(mut self, client_name: impl Into<String>) -> Self {
self.inner = self.inner.client_name(client_name);
self
}
pub fn connect(self) -> Result<KiCadClientBlocking, KiCadError> {
let core = BlockingCore::start()?;
let inner_builder = self.inner;
let inner = core.call(move |runtime| runtime.block_on(inner_builder.connect()))?;
Ok(KiCadClientBlocking { inner, core })
}
}
impl Default for KiCadClientBlockingBuilder {
fn default() -> Self {
Self::new()
}
}
macro_rules! blocking_methods {
(
$(fn $name:ident(&self $(, $arg:ident : $arg_ty:ty)*) -> $ret:ty;)+
) => {
$(
pub fn $name(&self, $($arg: $arg_ty),*) -> $ret {
let client = self.inner.clone();
self.core.call(move |runtime| runtime.block_on(async move {
client.$name($($arg),*).await
}))
}
)+
#[cfg(test)]
pub(crate) const GENERATED_BLOCKING_METHOD_NAMES: &'static [&'static str] = &[
$(stringify!($name),)+
];
};
}
impl KiCadClientBlocking {
pub fn builder() -> KiCadClientBlockingBuilder {
KiCadClientBlockingBuilder::new()
}
pub fn connect() -> Result<Self, KiCadError> {
KiCadClientBlockingBuilder::new().connect()
}
pub fn timeout(&self) -> Duration {
self.inner.timeout()
}
pub fn socket_uri(&self) -> &str {
self.inner.socket_uri()
}
pub fn inner(&self) -> &KiCadClient {
&self.inner
}
pub fn run_action_raw(&self, action: impl Into<String>) -> Result<Any, KiCadError> {
let action = action.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move { client.run_action_raw(action).await })
})
}
pub fn run_action(&self, action: impl Into<String>) -> Result<RunActionStatus, KiCadError> {
let action = action.into();
let client = self.inner.clone();
self.core
.call(move |runtime| runtime.block_on(async move { client.run_action(action).await }))
}
pub fn get_kicad_binary_path_raw(
&self,
binary_name: impl Into<String>,
) -> Result<Any, KiCadError> {
let binary_name = binary_name.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move { client.get_kicad_binary_path_raw(binary_name).await })
})
}
pub fn get_kicad_binary_path(
&self,
binary_name: impl Into<String>,
) -> Result<String, KiCadError> {
let binary_name = binary_name.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move { client.get_kicad_binary_path(binary_name).await })
})
}
pub fn get_plugin_settings_path_raw(
&self,
identifier: impl Into<String>,
) -> Result<Any, KiCadError> {
let identifier = identifier.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move { client.get_plugin_settings_path_raw(identifier).await })
})
}
pub fn get_plugin_settings_path(
&self,
identifier: impl Into<String>,
) -> Result<String, KiCadError> {
let identifier = identifier.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move { client.get_plugin_settings_path(identifier).await })
})
}
pub fn end_commit_raw(
&self,
session: CommitSession,
action: CommitAction,
message: impl Into<String>,
) -> Result<Any, KiCadError> {
let message = message.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move { client.end_commit_raw(session, action, message).await })
})
}
pub fn end_commit(
&self,
session: CommitSession,
action: CommitAction,
message: impl Into<String>,
) -> Result<(), KiCadError> {
let message = message.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move { client.end_commit(session, action, message).await })
})
}
pub fn parse_and_create_items_from_string_raw(
&self,
contents: impl Into<String>,
) -> Result<Any, KiCadError> {
let contents = contents.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move {
client
.parse_and_create_items_from_string_raw(contents)
.await
})
})
}
pub fn parse_and_create_items_from_string(
&self,
contents: impl Into<String>,
) -> Result<Vec<Any>, KiCadError> {
let contents = contents.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime
.block_on(async move { client.parse_and_create_items_from_string(contents).await })
})
}
pub fn inject_drc_error_raw(
&self,
severity: DrcSeverity,
message: impl Into<String>,
position: Option<Vector2Nm>,
item_ids: Vec<String>,
) -> Result<Any, KiCadError> {
let message = message.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move {
client
.inject_drc_error_raw(severity, message, position, item_ids)
.await
})
})
}
pub fn inject_drc_error(
&self,
severity: DrcSeverity,
message: impl Into<String>,
position: Option<Vector2Nm>,
item_ids: Vec<String>,
) -> Result<Option<String>, KiCadError> {
let message = message.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move {
client
.inject_drc_error(severity, message, position, item_ids)
.await
})
})
}
pub fn save_copy_of_document_raw(
&self,
path: impl Into<String>,
overwrite: bool,
include_project: bool,
) -> Result<Any, KiCadError> {
let path = path.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move {
client
.save_copy_of_document_raw(path, overwrite, include_project)
.await
})
})
}
pub fn save_copy_of_document(
&self,
path: impl Into<String>,
overwrite: bool,
include_project: bool,
) -> Result<(), KiCadError> {
let path = path.into();
let client = self.inner.clone();
self.core.call(move |runtime| {
runtime.block_on(async move {
client
.save_copy_of_document(path, overwrite, include_project)
.await
})
})
}
blocking_methods! {
fn ping(&self) -> Result<(), KiCadError>;
fn refresh_editor(&self, frame: EditorFrameType) -> Result<(), KiCadError>;
fn get_version(&self) -> Result<VersionInfo, KiCadError>;
fn get_open_documents(&self, document_type: DocumentType) -> Result<Vec<DocumentSpecifier>, KiCadError>;
fn get_net_classes_raw(&self) -> Result<Any, KiCadError>;
fn get_net_classes(&self) -> Result<Vec<NetClassInfo>, KiCadError>;
fn set_net_classes_raw(&self, net_classes: Vec<NetClassInfo>, merge_mode: MapMergeMode) -> Result<Any, KiCadError>;
fn set_net_classes(&self, net_classes: Vec<NetClassInfo>, merge_mode: MapMergeMode) -> Result<Vec<NetClassInfo>, KiCadError>;
fn get_text_variables_raw(&self) -> Result<Any, KiCadError>;
fn get_text_variables(&self) -> Result<BTreeMap<String, String>, KiCadError>;
fn set_text_variables_raw(&self, variables: BTreeMap<String, String>, merge_mode: MapMergeMode) -> Result<Any, KiCadError>;
fn set_text_variables(&self, variables: BTreeMap<String, String>, merge_mode: MapMergeMode) -> Result<BTreeMap<String, String>, KiCadError>;
fn expand_text_variables_raw(&self, text: Vec<String>) -> Result<Any, KiCadError>;
fn expand_text_variables(&self, text: Vec<String>) -> Result<Vec<String>, KiCadError>;
fn get_text_extents_raw(&self, text: TextSpec) -> Result<Any, KiCadError>;
fn get_text_extents(&self, text: TextSpec) -> Result<TextExtents, KiCadError>;
fn get_text_as_shapes_raw(&self, text: Vec<TextObjectSpec>) -> Result<Any, KiCadError>;
fn get_text_as_shapes(&self, text: Vec<TextObjectSpec>) -> Result<Vec<TextAsShapesEntry>, KiCadError>;
fn get_current_project_path(&self) -> Result<PathBuf, KiCadError>;
fn has_open_board(&self) -> Result<bool, KiCadError>;
fn begin_commit_raw(&self) -> Result<Any, KiCadError>;
fn begin_commit(&self) -> Result<CommitSession, KiCadError>;
fn create_items_raw(&self, items: Vec<Any>, container_id: Option<String>) -> Result<Any, KiCadError>;
fn create_items(&self, items: Vec<Any>, container_id: Option<String>) -> Result<Vec<Any>, KiCadError>;
fn update_items_raw(&self, items: Vec<Any>) -> Result<Any, KiCadError>;
fn update_items(&self, items: Vec<Any>) -> Result<Vec<Any>, KiCadError>;
fn delete_items_raw(&self, item_ids: Vec<String>) -> Result<Any, KiCadError>;
fn delete_items(&self, item_ids: Vec<String>) -> Result<Vec<String>, KiCadError>;
fn get_nets(&self) -> Result<Vec<BoardNet>, KiCadError>;
fn get_board_enabled_layers(&self) -> Result<BoardEnabledLayers, KiCadError>;
fn set_board_enabled_layers(&self, copper_layer_count: u32, layer_ids: Vec<i32>) -> Result<BoardEnabledLayers, KiCadError>;
fn get_active_layer(&self) -> Result<BoardLayerInfo, KiCadError>;
fn set_active_layer(&self, layer_id: i32) -> Result<(), KiCadError>;
fn get_visible_layers(&self) -> Result<Vec<BoardLayerInfo>, KiCadError>;
fn set_visible_layers(&self, layer_ids: Vec<i32>) -> Result<(), KiCadError>;
fn get_board_layer_name(&self, layer_id: i32) -> Result<String, KiCadError>;
fn get_board_origin(&self, kind: BoardOriginKind) -> Result<Vector2Nm, KiCadError>;
fn set_board_origin(&self, kind: BoardOriginKind, origin: Vector2Nm) -> Result<(), KiCadError>;
fn get_selection_summary(&self, type_codes: Vec<i32>) -> Result<SelectionSummary, KiCadError>;
fn get_selection_raw(&self, type_codes: Vec<i32>) -> Result<Vec<Any>, KiCadError>;
fn get_selection_details(&self, type_codes: Vec<i32>) -> Result<Vec<SelectionItemDetail>, KiCadError>;
fn get_selection(&self, type_codes: Vec<i32>) -> Result<Vec<PcbItem>, KiCadError>;
fn add_to_selection_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn add_to_selection(&self, item_ids: Vec<String>) -> Result<SelectionMutationResult, KiCadError>;
fn clear_selection_raw(&self) -> Result<Vec<Any>, KiCadError>;
fn clear_selection(&self) -> Result<SelectionMutationResult, KiCadError>;
fn remove_from_selection_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn remove_from_selection(&self, item_ids: Vec<String>) -> Result<SelectionMutationResult, KiCadError>;
fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError>;
fn get_vias_raw(&self) -> Result<Vec<Any>, KiCadError>;
fn get_vias(&self) -> Result<Vec<PcbVia>, KiCadError>;
fn get_items_raw_by_type_codes(&self, type_codes: Vec<i32>) -> Result<Vec<Any>, KiCadError>;
fn get_items_details_by_type_codes(&self, type_codes: Vec<i32>) -> Result<Vec<SelectionItemDetail>, KiCadError>;
fn get_items_by_type_codes(&self, type_codes: Vec<i32>) -> Result<Vec<PcbItem>, KiCadError>;
fn get_all_pcb_items_raw(&self) -> Result<Vec<(PcbObjectTypeCode, Vec<Any>)>, KiCadError>;
fn get_all_pcb_items_details(&self) -> Result<Vec<(PcbObjectTypeCode, Vec<SelectionItemDetail>)>, KiCadError>;
fn get_all_pcb_items(&self) -> Result<Vec<(PcbObjectTypeCode, Vec<PcbItem>)>, KiCadError>;
fn get_items_by_net_raw(&self, type_codes: Vec<i32>, net_codes: Vec<i32>) -> Result<Vec<Any>, KiCadError>;
fn get_items_by_net(&self, type_codes: Vec<i32>, net_codes: Vec<i32>) -> Result<Vec<PcbItem>, KiCadError>;
fn get_items_by_net_class_raw(&self, type_codes: Vec<i32>, net_classes: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn get_items_by_net_class(&self, type_codes: Vec<i32>, net_classes: Vec<String>) -> Result<Vec<PcbItem>, KiCadError>;
fn get_netclass_for_nets_raw(&self, nets: Vec<BoardNet>) -> Result<Any, KiCadError>;
fn get_netclass_for_nets(&self, nets: Vec<BoardNet>) -> Result<Vec<NetClassForNetEntry>, KiCadError>;
fn refill_zones(&self, zone_ids: Vec<String>) -> Result<(), KiCadError>;
fn get_pad_shape_as_polygon_raw(&self, pad_ids: Vec<String>, layer_id: i32) -> Result<Vec<Any>, KiCadError>;
fn get_pad_shape_as_polygon(&self, pad_ids: Vec<String>, layer_id: i32) -> Result<Vec<PadShapeAsPolygonEntry>, KiCadError>;
fn check_padstack_presence_on_layers_raw(&self, item_ids: Vec<String>, layer_ids: Vec<i32>) -> Result<Vec<Any>, KiCadError>;
fn check_padstack_presence_on_layers(&self, item_ids: Vec<String>, layer_ids: Vec<i32>) -> Result<Vec<PadstackPresenceEntry>, KiCadError>;
fn get_board_stackup_raw(&self) -> Result<Any, KiCadError>;
fn get_board_stackup(&self) -> Result<BoardStackup, KiCadError>;
fn update_board_stackup_raw(&self, stackup: BoardStackup) -> Result<Any, KiCadError>;
fn update_board_stackup(&self, stackup: BoardStackup) -> Result<BoardStackup, KiCadError>;
fn get_graphics_defaults_raw(&self) -> Result<Any, KiCadError>;
fn get_graphics_defaults(&self) -> Result<GraphicsDefaults, KiCadError>;
fn get_board_editor_appearance_settings_raw(&self) -> Result<Any, KiCadError>;
fn get_board_editor_appearance_settings(&self) -> Result<BoardEditorAppearanceSettings, KiCadError>;
fn set_board_editor_appearance_settings(&self, settings: BoardEditorAppearanceSettings) -> Result<BoardEditorAppearanceSettings, KiCadError>;
fn interactive_move_items_raw(&self, item_ids: Vec<String>) -> Result<Any, KiCadError>;
fn interactive_move_items(&self, item_ids: Vec<String>) -> Result<(), KiCadError>;
fn get_title_block_info(&self) -> Result<TitleBlockInfo, KiCadError>;
fn save_document_raw(&self) -> Result<Any, KiCadError>;
fn save_document(&self) -> Result<(), KiCadError>;
fn revert_document_raw(&self) -> Result<Any, KiCadError>;
fn revert_document(&self) -> Result<(), KiCadError>;
fn get_board_as_string(&self) -> Result<String, KiCadError>;
fn get_selection_as_string(&self) -> Result<SelectionStringDump, KiCadError>;
fn get_items_by_id_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn get_items_by_id_details(&self, item_ids: Vec<String>) -> Result<Vec<SelectionItemDetail>, KiCadError>;
fn get_items_by_id(&self, item_ids: Vec<String>) -> Result<Vec<PcbItem>, KiCadError>;
fn get_item_bounding_boxes(&self, item_ids: Vec<String>, include_child_text: bool) -> Result<Vec<ItemBoundingBox>, KiCadError>;
fn hit_test_item(&self, item_id: String, position: Vector2Nm, tolerance_nm: i32) -> Result<ItemHitTestResult, KiCadError>;
}
#[cfg(test)]
pub(crate) const MANUAL_BLOCKING_METHOD_NAMES: &'static [&'static str] = &[
"connect",
"run_action_raw",
"run_action",
"get_kicad_binary_path_raw",
"get_kicad_binary_path",
"get_plugin_settings_path_raw",
"get_plugin_settings_path",
"end_commit_raw",
"end_commit",
"parse_and_create_items_from_string_raw",
"parse_and_create_items_from_string",
"inject_drc_error_raw",
"inject_drc_error",
"save_copy_of_document_raw",
"save_copy_of_document",
];
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
use std::sync::mpsc as std_mpsc;
use std::time::{Duration, Instant};
#[test]
fn blocking_core_executes_job_and_returns_result() {
let core = BlockingCore::start().expect("blocking core must start");
let value = core
.call(|_| Ok::<_, KiCadError>(1234))
.expect("blocking job should execute");
assert_eq!(value, 1234);
}
#[test]
fn blocking_core_handles_concurrent_submitters() {
let core = BlockingCore::start().expect("blocking core must start");
let mut handles = Vec::new();
for idx in 0..8 {
let core = Arc::clone(&core);
handles.push(thread::spawn(move || {
core.call(move |_| Ok::<_, KiCadError>(idx * 2))
.expect("job should return");
}));
}
for handle in handles {
handle.join().expect("submitter thread must join");
}
}
#[test]
fn blocking_core_shutdown_drains_inflight_jobs() {
let core = BlockingCore::start().expect("blocking core must start");
let (started_tx, started_rx) = std_mpsc::sync_channel::<()>(1);
let core_for_call = Arc::clone(&core);
let worker = thread::spawn(move || {
core_for_call
.call(move |_| {
let _ = started_tx.send(());
thread::sleep(Duration::from_millis(120));
Ok::<_, KiCadError>(())
})
.expect("in-flight job should complete");
});
started_rx
.recv_timeout(Duration::from_secs(1))
.expect("job should begin");
let begin = Instant::now();
core.shutdown();
let elapsed = begin.elapsed();
assert!(
elapsed >= Duration::from_millis(80),
"shutdown should wait for in-flight job; elapsed: {elapsed:?}"
);
worker.join().expect("worker submitter should join");
}
#[test]
fn blocking_core_returns_closed_error_after_shutdown() {
let core = BlockingCore::start().expect("blocking core must start");
core.shutdown();
let err = core
.call(|_| Ok::<_, KiCadError>(()))
.expect_err("closed core should reject calls");
assert!(matches!(err, KiCadError::BlockingRuntimeClosed));
}
#[test]
fn sync_wrapper_covers_async_method_names() {
let mut async_methods = BTreeSet::new();
for line in include_str!("client.rs").lines() {
let trimmed = line.trim_start();
if let Some(rest) = trimmed.strip_prefix("pub async fn ") {
if let Some(name) = rest.split('(').next() {
async_methods.insert(name.trim().to_string());
}
}
}
let blocking_methods: BTreeSet<String> =
KiCadClientBlocking::GENERATED_BLOCKING_METHOD_NAMES
.iter()
.chain(KiCadClientBlocking::MANUAL_BLOCKING_METHOD_NAMES.iter())
.map(|name| (*name).to_string())
.collect();
let missing: Vec<String> = async_methods
.into_iter()
.filter(|name| !blocking_methods.contains(name))
.collect();
assert!(
missing.is_empty(),
"missing blocking wrappers for async methods: {:?}",
missing
);
}
#[test]
fn impl_into_string_wrapper_signatures_accept_str() {
fn assert_signatures(client: &KiCadClientBlocking) {
let _ = client.run_action_raw("pcbnew.Refresh");
let _ = client.run_action("pcbnew.Refresh");
let _ = client.get_kicad_binary_path_raw("kicad-cli");
let _ = client.get_kicad_binary_path("kicad-cli");
let _ = client.get_plugin_settings_path_raw("kicad-ipc-rs");
let _ = client.get_plugin_settings_path("kicad-ipc-rs");
let _ = client.end_commit_raw(
CommitSession {
id: "commit-id".to_string(),
},
CommitAction::Drop,
"test",
);
let _ = client.end_commit(
CommitSession {
id: "commit-id".to_string(),
},
CommitAction::Drop,
"test",
);
let _ = client.parse_and_create_items_from_string_raw("(kicad_pcb)");
let _ = client.parse_and_create_items_from_string("(kicad_pcb)");
let _ = client.inject_drc_error_raw(DrcSeverity::Warning, "marker", None, Vec::new());
let _ = client.inject_drc_error(DrcSeverity::Warning, "marker", None, Vec::new());
let _ = client.save_copy_of_document_raw("/tmp/example.kicad_pcb", false, false);
let _ = client.save_copy_of_document("/tmp/example.kicad_pcb", false, false);
}
let _ = assert_signatures as fn(&KiCadClientBlocking);
}
#[test]
fn blocking_smoke_live_when_socket_env_is_set() {
if std::env::var("KICAD_API_SOCKET").is_err() {
return;
}
let client = KiCadClientBlocking::connect().expect("blocking client should connect");
client.ping().expect("ping should succeed");
let _ = client.get_version().expect("version should succeed");
let _ = client
.get_open_documents(DocumentType::Pcb)
.expect("open docs should succeed");
let _ = client
.get_visible_layers()
.expect("board read method should succeed");
}
}