diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1dc6354 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ + +build: + maturin develop + # cargo build + +ui: build + cargo test --test ui_test + +.PHONY: build ui \ No newline at end of file diff --git a/src/exec/cli_api.rs b/src/exec/cli_api.rs index 587648b..1583e49 100644 --- a/src/exec/cli_api.rs +++ b/src/exec/cli_api.rs @@ -1,19 +1,27 @@ +//! The API for the CLI test. use std::error::Error; use crate::cli::tty::WrapperTty; +/// The API for the CLI test. pub trait CliTestApi: WrapperTty { - /// + /// Run a script in the terminal, wait for the script to finish(or timeout), and return the output. /// /// You may found this func includes assert_script_run and script_output fn script_run(&mut self, script: &str, timeout: u32) -> Result>; + + /// Run a script in the terminal which is a background command fn background_script_run(&mut self, script: &str) -> Result<(), Box>; + + /// Write someting to the terminal. fn writeln(&mut self, script: &str) -> Result<(), Box>; + + /// Wait for the terminal to output the expected string. Output the terminal output when the expected string is found. fn wait_serial(&mut self, expected: &str, timeout: u32) -> Result>; } pub trait SudoCliTestApi: CliTestApi { - /// + /// Just like script_run, but with sudo. /// /// You may found this func includes assert_script_sudo and script_output fn script_sudo(&mut self, script: &str, timeout: u32) -> Result>; diff --git a/src/exec/cli_exec.rs b/src/exec/cli_exec.rs index dedcc0b..ebdef1d 100644 --- a/src/exec/cli_exec.rs +++ b/src/exec/cli_exec.rs @@ -1,3 +1,5 @@ +//! The implementation of the CLI tester. Look at [`CliTestApi`] for more information. + use std::{ error::Error, thread::sleep, diff --git a/src/exec/gui_exec.rs b/src/exec/gui_exec.rs index d2e3021..44756ab 100644 --- a/src/exec/gui_exec.rs +++ b/src/exec/gui_exec.rs @@ -1,4 +1,4 @@ -//! Executor gor GUI +//! Executor for GUI. Look at [`GuiTestApi`] for more details. //! use std::{error::Error, time::Instant}; diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 3306382..ba519de 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -1,3 +1,10 @@ +//! How to execute some operations for the CLI and GUI. +//! +//! This part contains the interactive API for the CLI and GUI. You definitely +//! could use the CLI API and GUI API directly, but you really want to see a bunch of +//! blocking IO and u8 slices? +//! So these wrappers are here to provide high-level API for the CLI and GUI. + pub mod cli_api; pub mod cli_exec; pub mod gui_api; diff --git a/src/pythonapi/mod.rs b/src/pythonapi/mod.rs index 91fdea4..ec66034 100644 --- a/src/pythonapi/mod.rs +++ b/src/pythonapi/mod.rs @@ -29,6 +29,8 @@ use tee::Tee; use shell_like::PyTty; use util::{get_log_level, run_ui, set_log_level}; +use crate::ui::register_ui; + #[pymodule] #[pyo3(name = "tester")] fn tester(m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -52,5 +54,7 @@ fn tester(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(warn, m)?)?; m.add_function(wrap_pyfunction!(err, m)?)?; + register_ui(m)?; + Ok(()) } diff --git a/src/ui/main.rs b/src/ui/main.rs index d07e1a6..4c0fc75 100644 --- a/src/ui/main.rs +++ b/src/ui/main.rs @@ -1,6 +1,9 @@ //! Main UI render for the APP -use std::error::Error; +use std::{ + error::Error, + sync::{LazyLock, Mutex}, thread::{sleep, sleep_ms}, time::Duration, +}; use eframe::{ egui::{Context, Id, SidePanel, Ui, ViewportBuilder}, @@ -35,16 +38,19 @@ impl AppUi { } } -struct SubWindowHolder { - window: Box, - id: Id, - title: String, - open: bool, +pub struct SubWindowHolder { + pub window: Box, + pub id: Id, + pub title: String, + pub open: bool, } +pub static mut _sub_windows: LazyLock>> = + LazyLock::new(|| Mutex::new(Vec::new())); + struct MyApp { sub_window_creator: Vec>, // We ensure that the sub windows only work in the main thread - sub_windows: Vec, + // sub_windows: Vec, sub_window_idx: usize, } @@ -58,7 +64,7 @@ impl Default for MyApp { } Self { sub_window_creator, - sub_windows: Vec::new(), + // sub_windows: Vec::new(), sub_window_idx: 0, } } @@ -75,7 +81,9 @@ impl MyApp { let id = Id::new(self.sub_window_idx); info!("Try create sub window: {}", title); self.sub_window_idx += 1; - self.sub_windows.push(SubWindowHolder { + sleep(Duration::from_millis(5)); + let mut sub_windows = unsafe { _sub_windows.lock().unwrap() }; + sub_windows.push(SubWindowHolder { window: creator.open(), id, title, @@ -83,9 +91,13 @@ impl MyApp { }); } } - self.sub_windows.retain(|w| w.open); - for w in &mut self.sub_windows { - w.window.show(ctx, &w.title, &w.id, &mut w.open); + { + sleep(Duration::from_millis(5)); + let mut sub_windows = unsafe { _sub_windows.lock().unwrap() }; + sub_windows.retain(|w| w.open); + for w in sub_windows.iter_mut() { + w.window.show(ctx, &w.title, &w.id, &mut w.open); + } } }); } @@ -120,15 +132,15 @@ pub trait SubWindowCreator { } /// Snippet to register a sub window -/// +/// /// # Arguments /// $name: The struct name of the sub window /// $window_name: The name of the window, will become the title of the window -/// +/// /// # Example /// `impl_sub_window!(TestUiStruct, "TestUiName");` /// where TestUiStruct implements SubWindow trait and Default trait -/// +/// /// # Notice /// If you found rust-analyzer gives "invalid metavariable expression", this is a nightly feature, you can ignore it. It will work. /// The problem is on `${concat()}` macro. Just suppress it. diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 56bf727..86329b0 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -9,4 +9,16 @@ pub mod main; pub mod code_editor; pub mod pyenv; pub mod terminal; -pub mod cli_hooker; \ No newline at end of file +pub mod cli_hooker; +pub mod ui_cli_exec; + +use pyo3::{types::{PyModule, PyModuleMethods}, Bound, PyResult}; +use ui_cli_exec::UiExec; + +pub fn register_ui(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let m = PyModule::new_bound(parent_module.py(), "ui")?; + m.add_class::()?; + + parent_module.add_submodule(&m)?; + Ok(()) +} diff --git a/src/ui/ui_cli_exec.rs b/src/ui/ui_cli_exec.rs new file mode 100644 index 0000000..4cf3ed0 --- /dev/null +++ b/src/ui/ui_cli_exec.rs @@ -0,0 +1,487 @@ +use std::{ + error::Error, + sync::{ + mpsc::{self, Receiver, Sender}, + Arc, + }, + thread::{sleep, spawn, JoinHandle}, + time::{Duration, Instant}, +}; + +use eframe::egui::{mutex::Mutex, Id}; +use pyo3::{exceptions::PyRuntimeError, pyclass, pymethods, PyRefMut, PyResult}; + +use crate::{ + cli::tty::{DynTty, Tty, WrapperTty}, + consts::DURATION, + err, + exec::cli_api::{CliTestApi, SudoCliTestApi}, + impl_any, info, + pythonapi::shell_like::{handle_wrap, py_tty_inner, PyTty, PyTtyInner}, + util::{anybase::heap_raw, util::rand_string}, +}; + +use super::{ + main::_sub_windows, + terminal::{Terminal, TerminalMessage}, +}; + +pub struct UiCliTester { + inner: Arc>>, + send: Arc>>>, + recv: Arc>>>, + buf: Arc>>, + handle: Option>, + exit: Arc>, +} + +impl UiCliTester { + pub fn try_hook(&mut self, term: &mut Terminal) -> Result<(), Box> { + let send = self.send.clone(); + let recv = self.recv.clone(); + let mut send = send.lock(); + let mut recv = recv.lock(); + if send.is_some() || recv.is_some() { + err!("Already hooked"); + return Err("Already hooked".into()); + } + let (tx, rx) = mpsc::channel(); + *send = Some(tx); + let o_rx = term.try_hook(rx)?; + *recv = Some(o_rx); + Ok(()) + } + pub fn build(inner: DynTty, term: &mut Terminal) -> Result> { + let mut res = Self { + inner: Arc::new(Mutex::new(Some(inner))), + send: Arc::new(Mutex::new(None)), + recv: Arc::new(Mutex::new(None)), + buf: Arc::new(Mutex::new(Vec::new())), + handle: None, + exit: Arc::new(Mutex::new(false)), + }; + res.try_hook(term)?; + + let inner = res.inner.clone(); + let send = res.send.clone(); + let recv = res.recv.clone(); + let buf = res.buf.clone(); + let exit = res.exit.clone(); + let handle = spawn(move || loop { + { + let exit = exit.lock(); + if *exit { + break; + } + } + let data; + { + let mut inner = inner.lock(); + if inner.is_none() { + continue; + } + let inner = inner.as_mut().unwrap(); + let d = inner.read(); + if let Err(e) = d { + err!("read error: {}", e); + break; + } + data = d.unwrap(); + let mut buf = buf.lock(); + buf.extend(data.clone()); + } + { + let mut recv_o = recv.lock(); + if recv_o.is_none() { + continue; + } + let recv = recv_o.as_mut().unwrap(); + match recv.try_recv() { + Ok(TerminalMessage::Data(_)) => { + unreachable!() + } + Ok(TerminalMessage::Close) => { + let mut send_o = send.lock(); + let send = send_o.as_ref().unwrap(); + send.send(TerminalMessage::Close).unwrap(); + recv_o.take(); + send_o.take(); + } + Err(_) => { + continue; + } + } + } + let send_o = send.lock(); + let send = send_o.as_ref().unwrap(); + send.send(TerminalMessage::Data(data)).unwrap(); + }); + + res.handle = Some(handle); + + Ok(res) + } +} + +impl UiCliTester { + fn run_command(&mut self, command: &String) -> Result<(), Box> { + info!("Write to shell: {}", command); + sleep(Duration::from_millis(DURATION)); + let inner = self.inner.clone(); + let mut inner = inner.lock(); + let inner = inner.as_mut().unwrap(); + inner.write(command.as_bytes()) + } + fn __exit(&mut self) { + let mut send_o = self.send.lock(); + if send_o.is_some() { + let send = send_o.as_ref().unwrap(); + send.send(TerminalMessage::Close).unwrap(); + } + send_o.take(); + let mut recv_o = self.recv.lock(); + recv_o.take(); + let mut exit = self.exit.lock(); + *exit = true; + } +} +// impl Drop for UiCliTester { +// fn drop(&mut self) { +// self.__exit(); +// } +// } +impl_any!(UiCliTester); +impl Tty for UiCliTester { + fn read(&mut self) -> Result, Box> { + let mut buf = self.buf.lock(); + let res = buf.clone(); + buf.clear(); + Ok(res) + } + fn read_line(&mut self) -> Result, Box> { + let mut buf = self.buf.lock(); + let res = buf.clone(); + buf.clear(); + Ok(res) + } + fn write(&mut self, data: &[u8]) -> Result<(), Box> { + let inner = self.inner.clone(); + let mut inner = inner.lock(); + let inner = inner.as_mut().unwrap(); + inner.write(data) + } +} +impl WrapperTty for UiCliTester { + fn exit(mut self) -> DynTty { + self.__exit(); + self.inner.lock().take().unwrap() + } + + fn inner_ref(&self) -> &DynTty { + // &self.inner + panic!("You should not call this method"); + } + + fn inner_mut(&mut self) -> &mut DynTty { + // &mut self.inner + panic!("You should not call this method"); + } +} + +impl UiCliTester { + fn filter_assert_echo(&self, expected: &str, buf: &mut Vec) -> Result<(), Box> { + let expected = "echo ".to_owned() + expected; + let expected = expected.as_bytes(); + for (pos, window) in buf.windows(expected.len()).enumerate() { + if window == expected { + let i = pos + expected.len(); + buf.drain(0..=i); + break; + } + } + Ok(()) + } + + fn kmp_next(&self, target: &Vec) -> Vec { + let mut next = vec![0usize; target.len()]; + let mut i = 1; + let mut j = 0; + while i < target.len() - 1 { + if target[i] == target[j] { + next[i] = j + 1; + i += 1; + j += 1; + } else { + if j == 0 { + next[i] = 0; + i += 1; + } else { + j = next[j - 1] as usize; + } + } + } + next + } + + fn kmp_search(&self, content: &Vec, target: &Vec) -> Option { + let next = self.kmp_next(target); + let mut i = 0; + let mut j = 0; + let mut res = None; + while i < content.len() && j < target.len() { + if content[i] == target[j] { + if res.is_none() { + res = Some(i); + } + i += 1; + j += 1; + if j >= target.len() { + break; + } + } else { + if j == 0 { + i += 1; + } else { + j = next[j - 1]; + } + res = None; + } + } + res + } + + fn do_wait_serial( + &mut self, + expected: &str, + timeout: u32, + filter_echo_back: Option<&str>, + ) -> Result> { + let begin = Instant::now(); + info!("Waiting for string {{{}}}", expected); + loop { + sleep(Duration::from_millis(DURATION)); + let buf = self.buf.lock(); + let mut buf = buf.clone(); + if let Some(filter) = filter_echo_back { + self.filter_assert_echo(filter, &mut buf)?; + } + // The reason we compare raw u8 is... What if the data is corrupted? + let target = expected.as_bytes(); + if let Some(pos) = self.kmp_search(&buf, &target.to_vec()) { + info!("Matched string {{{}}}", expected); + let res = buf.split_off(pos + target.len()); + let res = String::from_utf8(res)?; + buf.drain(0..pos + target.len()); + return Ok(res); + } + if begin.elapsed().as_secs() > timeout as u64 { + err!( + "Timeout! Expected: {}, Actual: {}", + expected, + String::from_utf8(buf.clone()).unwrap() + ); + return Err(Box::::from("Timeout")); + } + } + } +} +impl CliTestApi for UiCliTester { + fn wait_serial(&mut self, expected: &str, timeout: u32) -> Result> { + self.do_wait_serial(expected, timeout, None) + } + fn script_run(&mut self, script: &str, timeout: u32) -> Result> { + let mut cmd = script.to_owned(); + let echo_content_rand = String::from_utf8(rand_string(8)).unwrap(); + + cmd += " && echo "; + cmd += &echo_content_rand; + cmd += " \n"; + + self.run_command(&cmd)?; + + self.do_wait_serial(&echo_content_rand, timeout, Some(&echo_content_rand)) + } + fn background_script_run(&mut self, script: &str) -> Result<(), Box> { + let mut cmd = script.to_owned(); + cmd += " &\n"; + self.run_command(&cmd) + } + fn writeln(&mut self, script: &str) -> Result<(), Box> { + let mut cmd = script.to_owned(); + cmd += "\n"; + self.run_command(&cmd) + } +} +impl SudoCliTestApi for UiCliTester { + fn script_sudo( + &mut self, + script: &str, + timeout: u32, + ) -> Result> { + let mut cmd = String::from("sudo "); + cmd += script; + cmd += " "; + self.script_run(&cmd, timeout) + } +} + +pub fn handle_uiclitester(inner: &mut Option, term_id: usize) -> PyResult<()> { + if inner.is_none() { + return Err(PyRuntimeError::new_err( + "You must define at least one valid object", + )); + } + let mut be_wrapped = inner.take().unwrap(); + let tty = be_wrapped.safe_take()?; + let tty = Box::into_inner(tty); + { + let mut sub_windows = + unsafe { _sub_windows.lock() }.map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + info!("Current sub_windows: {}", sub_windows.len()); + for window in sub_windows.iter_mut() { + info!("Checking window: {} {} {}", window.title, window.id.value(), term_id); + if window.id.value() != term_id as u64 { + continue; + } + let window = &mut window.window; + let term = window.as_any_mut().downcast_mut::(); + if term.is_none() { + return Err(PyRuntimeError::new_err("Can't find the terminal")); + } + let term = term.unwrap(); + let res = UiCliTester::build(tty, term) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + let res = Box::new(res); + *inner = Some(py_tty_inner(heap_raw(res))); + return Ok(()); + } + return Err(PyRuntimeError::new_err("Can't find the terminal")); + } +} + +#[pyclass(extends=PyTty, subclass)] +pub struct UiExec {} + +#[pymethods] +impl UiExec { + #[new] + #[pyo3(signature = (be_wrapped, term_id))] + fn py_new(be_wrapped: &mut PyTty, term_id: usize) -> PyResult<(Self, PyTty)> { + let mut inner = None; + + handle_wrap(&mut inner, Some(be_wrapped))?; + handle_uiclitester(&mut inner, term_id)?; + + Ok((UiExec {}, PyTty::build(inner.unwrap()))) + } + #[pyo3(signature = (script, timeout=None))] + fn script_run( + mut self_: PyRefMut<'_, Self>, + script: &str, + timeout: Option, + ) -> PyResult { + let self_ = self_.as_mut(); + let inner = self_.inner.get_mut()?; + let inner = inner.as_any_mut(); + + let timeout = timeout.unwrap_or(30); + + if inner.downcast_ref::().is_some() { + let inner = inner.downcast_mut::().unwrap(); + let res = inner + .script_run(script, timeout) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + Ok(res) + } else { + Err(PyRuntimeError::new_err( + "Can't find the right object to run the script", + )) + } + } + + fn background_script_run(mut self_: PyRefMut<'_, Self>, script: &str) -> PyResult<()> { + let self_ = self_.as_mut(); + let inner = self_.inner.get_mut()?; + let inner = inner.as_any_mut(); + + if inner.downcast_ref::().is_some() { + let inner = inner.downcast_mut::().unwrap(); + inner + .background_script_run(script) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + } else { + return Err(PyRuntimeError::new_err( + "Can't find the right object to run the script", + )); + } + Ok(()) + } + + fn writeln(mut self_: PyRefMut<'_, Self>, script: &str) -> PyResult<()> { + let self_ = self_.as_mut(); + let inner = self_.inner.get_mut()?; + let inner = inner.as_any_mut(); + + if inner.downcast_ref::().is_some() { + let inner = inner.downcast_mut::().unwrap(); + inner + .writeln(script) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + } else { + return Err(PyRuntimeError::new_err( + "Can't find the right object to run the script", + )); + } + Ok(()) + } + + #[pyo3(signature = (expected, timeout=None))] + fn wait_serial( + mut self_: PyRefMut<'_, Self>, + expected: &str, + timeout: Option, + ) -> PyResult { + let self_ = self_.as_mut(); + let inner = self_.inner.get_mut()?; + let inner = inner.as_any_mut(); + + let timeout = timeout.unwrap_or(30); + + if inner.downcast_ref::().is_some() { + let inner = inner.downcast_mut::().unwrap(); + let res = inner + .wait_serial(expected, timeout) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + Ok(res) + } else { + Err(PyRuntimeError::new_err( + "Can't find the right object to run the script", + )) + } + } + + #[pyo3(signature = (script, timeout=None))] + fn script_sudo( + mut self_: PyRefMut<'_, Self>, + script: &str, + timeout: Option, + ) -> PyResult { + let self_ = self_.as_mut(); + let inner = self_.inner.get_mut()?; + let inner = inner.as_any_mut(); + + let timeout = timeout.unwrap_or(30); + + if inner.downcast_ref::().is_some() { + let inner = inner.downcast_mut::().unwrap(); + let res = inner + .script_sudo(script, timeout) + .map_err(|e| PyRuntimeError::new_err(e.to_string()))?; + Ok(res) + } else { + Err(PyRuntimeError::new_err( + "Can't find the right object to run the script", + )) + } + } +}