1use crate::api_server::ApiClient;
44use crate::error::{IpcError, Result};
45use crate::socket_server::SocketServerConfig;
46use crate::task_manager::CancellationToken;
47use parking_lot::RwLock;
48use serde::{Deserialize, Serialize};
49use std::io::{BufRead, BufReader, Write};
50use std::process::{Child, Command, ExitStatus, Stdio};
51use std::sync::atomic::{AtomicBool, Ordering};
52use std::sync::Arc;
53use std::thread::{self, JoinHandle};
54use std::time::{Duration, Instant};
55
56#[derive(Clone)]
58pub struct CliBridgeConfig {
59 pub server_url: String,
61 pub auto_register: bool,
63 pub capture_stdout: bool,
65 pub capture_stderr: bool,
67 pub progress_parser: Option<Arc<dyn ProgressParser>>,
69 pub connect_timeout: Duration,
71 pub retry_count: u32,
73 pub retry_delay: Duration,
75}
76
77impl std::fmt::Debug for CliBridgeConfig {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 f.debug_struct("CliBridgeConfig")
80 .field("server_url", &self.server_url)
81 .field("auto_register", &self.auto_register)
82 .field("capture_stdout", &self.capture_stdout)
83 .field("capture_stderr", &self.capture_stderr)
84 .field("progress_parser", &self.progress_parser.is_some())
85 .field("connect_timeout", &self.connect_timeout)
86 .field("retry_count", &self.retry_count)
87 .field("retry_delay", &self.retry_delay)
88 .finish()
89 }
90}
91
92impl Default for CliBridgeConfig {
93 fn default() -> Self {
94 Self {
95 server_url: SocketServerConfig::default().path,
96 auto_register: true,
97 capture_stdout: true,
98 capture_stderr: true,
99 progress_parser: None,
100 connect_timeout: Duration::from_secs(5),
101 retry_count: 3,
102 retry_delay: Duration::from_millis(500),
103 }
104 }
105}
106
107impl CliBridgeConfig {
108 pub fn with_server(url: &str) -> Self {
110 Self {
111 server_url: url.to_string(),
112 ..Default::default()
113 }
114 }
115
116 pub fn progress_parser<P: ProgressParser + 'static>(mut self, parser: P) -> Self {
118 self.progress_parser = Some(Arc::new(parser));
119 self
120 }
121
122 pub fn no_auto_register(mut self) -> Self {
124 self.auto_register = false;
125 self
126 }
127
128 pub fn from_env() -> Self {
130 let mut config = Self::default();
131
132 if let Ok(url) = std::env::var("IPCKIT_SERVER_URL") {
133 config.server_url = url;
134 }
135
136 if let Ok(auto_reg) = std::env::var("IPCKIT_AUTO_REGISTER") {
137 config.auto_register = auto_reg.to_lowercase() != "false";
138 }
139
140 config
141 }
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct ProgressInfo {
147 pub current: u64,
149 pub total: u64,
151 pub message: Option<String>,
153}
154
155impl ProgressInfo {
156 pub fn new(current: u64, total: u64) -> Self {
158 Self {
159 current,
160 total,
161 message: None,
162 }
163 }
164
165 pub fn with_message(current: u64, total: u64, message: &str) -> Self {
167 Self {
168 current,
169 total,
170 message: Some(message.to_string()),
171 }
172 }
173
174 pub fn percentage(&self) -> u8 {
176 (self.current * 100)
177 .checked_div(self.total)
178 .map(|p| p.min(100) as u8)
179 .unwrap_or(0)
180 }
181}
182
183pub trait ProgressParser: Send + Sync {
185 fn parse(&self, line: &str) -> Option<ProgressInfo>;
187}
188
189pub mod parsers {
191 use super::*;
192 use regex::Regex;
193 use std::sync::LazyLock;
194
195 #[derive(Debug, Clone, Default)]
197 pub struct PercentageParser;
198
199 impl ProgressParser for PercentageParser {
200 fn parse(&self, line: &str) -> Option<ProgressInfo> {
201 static RE: LazyLock<Regex> =
202 LazyLock::new(|| Regex::new(r"(\d{1,3})%").expect("Invalid regex"));
203
204 RE.captures(line).and_then(|caps| {
205 caps.get(1)
206 .and_then(|m| m.as_str().parse::<u64>().ok())
207 .map(|pct| ProgressInfo::new(pct.min(100), 100))
208 })
209 }
210 }
211
212 #[derive(Debug, Clone, Default)]
214 pub struct FractionParser;
215
216 impl ProgressParser for FractionParser {
217 fn parse(&self, line: &str) -> Option<ProgressInfo> {
218 static RE: LazyLock<Regex> =
219 LazyLock::new(|| Regex::new(r"(\d+)\s*/\s*(\d+)").expect("Invalid regex"));
220
221 RE.captures(line).and_then(|caps| {
222 let current = caps.get(1)?.as_str().parse::<u64>().ok()?;
223 let total = caps.get(2)?.as_str().parse::<u64>().ok()?;
224 Some(ProgressInfo::new(current, total))
225 })
226 }
227 }
228
229 #[derive(Debug, Clone, Default)]
231 pub struct ProgressBarParser;
232
233 impl ProgressParser for ProgressBarParser {
234 fn parse(&self, line: &str) -> Option<ProgressInfo> {
235 static RE: LazyLock<Regex> = LazyLock::new(|| {
236 Regex::new(r"\[([=\-#>]+)\s*\]\s*(\d{1,3})%").expect("Invalid regex")
237 });
238
239 RE.captures(line).and_then(|caps| {
240 caps.get(2)
241 .and_then(|m| m.as_str().parse::<u64>().ok())
242 .map(|pct| ProgressInfo::new(pct.min(100), 100))
243 })
244 }
245 }
246
247 #[derive(Default)]
249 pub struct CompositeParser {
250 parsers: Vec<Arc<dyn ProgressParser>>,
251 }
252
253 impl CompositeParser {
254 pub fn new() -> Self {
256 Self::default()
257 }
258
259 #[allow(clippy::should_implement_trait)]
261 pub fn add<P: ProgressParser + 'static>(mut self, parser: P) -> Self {
262 self.parsers.push(Arc::new(parser));
263 self
264 }
265
266 pub fn default_all() -> Self {
268 Self::new()
269 .add(PercentageParser)
270 .add(FractionParser)
271 .add(ProgressBarParser)
272 }
273 }
274
275 impl ProgressParser for CompositeParser {
276 fn parse(&self, line: &str) -> Option<ProgressInfo> {
277 for parser in &self.parsers {
278 if let Some(info) = parser.parse(line) {
279 return Some(info);
280 }
281 }
282 None
283 }
284 }
285}
286
287struct BridgeState {
289 task_id: Option<String>,
290 task_name: Option<String>,
291 task_type: Option<String>,
292 progress: u8,
293 progress_message: Option<String>,
294 cancelled: AtomicBool,
295 completed: AtomicBool,
296}
297
298impl Default for BridgeState {
299 fn default() -> Self {
300 Self {
301 task_id: None,
302 task_name: None,
303 task_type: None,
304 progress: 0,
305 progress_message: None,
306 cancelled: AtomicBool::new(false),
307 completed: AtomicBool::new(false),
308 }
309 }
310}
311
312pub struct CliBridge {
314 config: CliBridgeConfig,
315 client: Option<ApiClient>,
316 state: Arc<RwLock<BridgeState>>,
317 cancel_token: CancellationToken,
318}
319
320impl CliBridge {
321 pub fn new(config: CliBridgeConfig) -> Result<Self> {
323 Ok(Self {
324 config,
325 client: None,
326 state: Arc::new(RwLock::new(BridgeState::default())),
327 cancel_token: CancellationToken::new(),
328 })
329 }
330
331 pub fn connect() -> Result<Self> {
333 Self::connect_with_config(CliBridgeConfig::from_env())
334 }
335
336 pub fn connect_with_config(config: CliBridgeConfig) -> Result<Self> {
338 let client = ApiClient::new(&config.server_url);
339
340 Ok(Self {
341 config,
342 client: Some(client),
343 state: Arc::new(RwLock::new(BridgeState::default())),
344 cancel_token: CancellationToken::new(),
345 })
346 }
347
348 pub fn register_task(&self, name: &str, task_type: &str) -> Result<String> {
350 let task_id = format!(
351 "cli-{}-{}",
352 std::process::id(),
353 std::time::SystemTime::now()
354 .duration_since(std::time::UNIX_EPOCH)
355 .unwrap_or_default()
356 .as_millis()
357 );
358
359 {
360 let mut state = self.state.write();
361 state.task_id = Some(task_id.clone());
362 state.task_name = Some(name.to_string());
363 state.task_type = Some(task_type.to_string());
364 }
365
366 if let Some(ref client) = self.client {
368 let _ = client.post(
369 "/v1/tasks",
370 Some(serde_json::json!({
371 "id": task_id,
372 "name": name,
373 "type": task_type,
374 "status": "running"
375 })),
376 );
377 }
378
379 Ok(task_id)
380 }
381
382 pub fn task_id(&self) -> Option<String> {
384 self.state.read().task_id.clone()
385 }
386
387 pub fn set_progress(&self, progress: u8, message: Option<&str>) {
389 let progress = progress.min(100);
390
391 {
392 let mut state = self.state.write();
393 state.progress = progress;
394 if let Some(msg) = message {
395 state.progress_message = Some(msg.to_string());
396 }
397 }
398
399 if let (Some(ref client), Some(task_id)) = (&self.client, self.task_id()) {
401 let _ = client.post(
402 &format!("/v1/tasks/{}/progress", task_id),
403 Some(serde_json::json!({
404 "progress": progress,
405 "message": message
406 })),
407 );
408 }
409 }
410
411 pub fn log(&self, level: &str, message: &str) {
413 eprintln!("[{}] {}", level.to_uppercase(), message);
415
416 if let (Some(ref client), Some(task_id)) = (&self.client, self.task_id()) {
418 let _ = client.post(
419 &format!("/v1/tasks/{}/logs", task_id),
420 Some(serde_json::json!({
421 "level": level,
422 "message": message
423 })),
424 );
425 }
426 }
427
428 pub fn stdout(&self, line: &str) {
430 println!("{}", line);
431
432 if let (Some(ref client), Some(task_id)) = (&self.client, self.task_id()) {
433 let _ = client.post(
434 &format!("/v1/tasks/{}/stdout", task_id),
435 Some(serde_json::json!({ "line": line })),
436 );
437 }
438 }
439
440 pub fn stderr(&self, line: &str) {
442 eprintln!("{}", line);
443
444 if let (Some(ref client), Some(task_id)) = (&self.client, self.task_id()) {
445 let _ = client.post(
446 &format!("/v1/tasks/{}/stderr", task_id),
447 Some(serde_json::json!({ "line": line })),
448 );
449 }
450 }
451
452 pub fn is_cancelled(&self) -> bool {
454 self.cancel_token.is_cancelled() || self.state.read().cancelled.load(Ordering::SeqCst)
455 }
456
457 pub fn cancel_token(&self) -> CancellationToken {
459 self.cancel_token.clone()
460 }
461
462 pub fn complete(&self, result: serde_json::Value) {
464 self.state.write().completed.store(true, Ordering::SeqCst);
465
466 if let (Some(ref client), Some(task_id)) = (&self.client, self.task_id()) {
467 let _ = client.post(
468 &format!("/v1/tasks/{}/complete", task_id),
469 Some(serde_json::json!({ "result": result })),
470 );
471 }
472 }
473
474 pub fn fail(&self, error: &str) {
476 self.state.write().completed.store(true, Ordering::SeqCst);
477
478 if let (Some(ref client), Some(task_id)) = (&self.client, self.task_id()) {
479 let _ = client.post(
480 &format!("/v1/tasks/{}/fail", task_id),
481 Some(serde_json::json!({ "error": error })),
482 );
483 }
484 }
485
486 pub fn wrap_stdout(&self) -> WrappedWriter {
488 WrappedWriter::new(
489 self.config.server_url.clone(),
490 self.task_id(),
491 OutputType::Stdout,
492 self.config.progress_parser.clone(),
493 Arc::clone(&self.state),
494 )
495 }
496
497 pub fn wrap_stderr(&self) -> WrappedWriter {
499 WrappedWriter::new(
500 self.config.server_url.clone(),
501 self.task_id(),
502 OutputType::Stderr,
503 None,
504 Arc::clone(&self.state),
505 )
506 }
507}
508
509#[derive(Debug, Clone, Copy, PartialEq, Eq)]
511pub enum OutputType {
512 Stdout,
513 Stderr,
514}
515
516pub struct WrappedWriter {
518 client: Option<ApiClient>,
519 task_id: Option<String>,
520 output_type: OutputType,
521 progress_parser: Option<Arc<dyn ProgressParser>>,
522 state: Arc<RwLock<BridgeState>>,
523 buffer: Vec<u8>,
524}
525
526impl WrappedWriter {
527 fn new(
528 server_url: String,
529 task_id: Option<String>,
530 output_type: OutputType,
531 progress_parser: Option<Arc<dyn ProgressParser>>,
532 state: Arc<RwLock<BridgeState>>,
533 ) -> Self {
534 let client = Some(ApiClient::new(&server_url));
535 Self {
536 client,
537 task_id,
538 output_type,
539 progress_parser,
540 state,
541 buffer: Vec::new(),
542 }
543 }
544
545 fn process_line(&mut self, line: &str) {
546 if let Some(ref parser) = self.progress_parser {
548 if let Some(info) = parser.parse(line) {
549 let mut state = self.state.write();
550 state.progress = info.percentage();
551 state.progress_message = info.message.clone();
552 }
553 }
554
555 if let (Some(ref client), Some(ref task_id)) = (&self.client, &self.task_id) {
557 let endpoint = match self.output_type {
558 OutputType::Stdout => format!("/v1/tasks/{}/stdout", task_id),
559 OutputType::Stderr => format!("/v1/tasks/{}/stderr", task_id),
560 };
561 let _ = client.post(&endpoint, Some(serde_json::json!({ "line": line })));
562 }
563 }
564}
565
566impl Write for WrappedWriter {
567 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
568 let written = match self.output_type {
570 OutputType::Stdout => std::io::stdout().write(buf)?,
571 OutputType::Stderr => std::io::stderr().write(buf)?,
572 };
573
574 self.buffer.extend_from_slice(&buf[..written]);
576
577 while let Some(pos) = self.buffer.iter().position(|&b| b == b'\n') {
579 let line = String::from_utf8_lossy(&self.buffer[..pos]).to_string();
580 self.buffer.drain(..=pos);
581 self.process_line(&line);
582 }
583
584 Ok(written)
585 }
586
587 fn flush(&mut self) -> std::io::Result<()> {
588 if !self.buffer.is_empty() {
590 let line = String::from_utf8_lossy(&self.buffer).to_string();
591 self.buffer.clear();
592 self.process_line(&line);
593 }
594
595 match self.output_type {
596 OutputType::Stdout => std::io::stdout().flush(),
597 OutputType::Stderr => std::io::stderr().flush(),
598 }
599 }
600}
601
602#[derive(Debug)]
604pub struct CommandOutput {
605 pub exit_code: i32,
607 pub stdout: String,
609 pub stderr: String,
611 pub duration: Duration,
613}
614
615pub struct WrappedCommand {
617 command: Command,
618 task_name: String,
619 task_type: String,
620 progress_parser: Option<Arc<dyn ProgressParser>>,
621 bridge_config: CliBridgeConfig,
622}
623
624impl WrappedCommand {
625 pub fn new(program: &str) -> Self {
627 let mut command = Command::new(program);
628 command.stdout(Stdio::piped()).stderr(Stdio::piped());
629
630 Self {
631 command,
632 task_name: program.to_string(),
633 task_type: "command".to_string(),
634 progress_parser: None,
635 bridge_config: CliBridgeConfig::from_env(),
636 }
637 }
638
639 pub fn task(mut self, name: &str, task_type: &str) -> Self {
641 self.task_name = name.to_string();
642 self.task_type = task_type.to_string();
643 self
644 }
645
646 pub fn arg(mut self, arg: &str) -> Self {
648 self.command.arg(arg);
649 self
650 }
651
652 pub fn args<I, S>(mut self, args: I) -> Self
654 where
655 I: IntoIterator<Item = S>,
656 S: AsRef<std::ffi::OsStr>,
657 {
658 self.command.args(args);
659 self
660 }
661
662 pub fn current_dir(mut self, dir: &std::path::Path) -> Self {
664 self.command.current_dir(dir);
665 self
666 }
667
668 pub fn env(mut self, key: &str, value: &str) -> Self {
670 self.command.env(key, value);
671 self
672 }
673
674 pub fn progress_parser<P: ProgressParser + 'static>(mut self, parser: P) -> Self {
676 self.progress_parser = Some(Arc::new(parser));
677 self
678 }
679
680 pub fn bridge_config(mut self, config: CliBridgeConfig) -> Self {
682 self.bridge_config = config;
683 self
684 }
685
686 pub fn run(mut self) -> Result<CommandOutput> {
688 let start = Instant::now();
689
690 let bridge = CliBridge::connect_with_config(self.bridge_config.clone()).ok();
692
693 if let Some(ref bridge) = bridge {
695 let _ = bridge.register_task(&self.task_name, &self.task_type);
696 }
697
698 let mut child = self.command.spawn().map_err(IpcError::Io)?;
700
701 let stdout = child.stdout.take();
703 let stderr = child.stderr.take();
704
705 let progress_parser = self.progress_parser.clone();
706 let bridge_clone = bridge.as_ref().map(|b| b.state.clone());
707
708 let stdout_handle: Option<JoinHandle<String>> = stdout.map(|out| {
710 let parser = progress_parser.clone();
711 let state = bridge_clone.clone();
712 thread::spawn(move || {
713 let mut output = String::new();
714 let reader = BufReader::new(out);
715 for line_result in reader.lines() {
716 let Ok(line) = line_result else { break };
717 println!("{}", line);
718 output.push_str(&line);
719 output.push('\n');
720
721 if let (Some(ref parser), Some(ref state)) = (&parser, &state) {
723 if let Some(info) = parser.parse(&line) {
724 let mut s = state.write();
725 s.progress = info.percentage();
726 s.progress_message = info.message;
727 }
728 }
729 }
730 output
731 })
732 });
733
734 let stderr_handle: Option<JoinHandle<String>> = stderr.map(|err| {
736 thread::spawn(move || {
737 let mut output = String::new();
738 let reader = BufReader::new(err);
739 for line_result in reader.lines() {
740 let Ok(line) = line_result else { break };
741 eprintln!("{}", line);
742 output.push_str(&line);
743 output.push('\n');
744 }
745 output
746 })
747 });
748
749 let status = child.wait().map_err(IpcError::Io)?;
751
752 let stdout_output = stdout_handle
754 .map(|h| h.join().unwrap_or_default())
755 .unwrap_or_default();
756 let stderr_output = stderr_handle
757 .map(|h| h.join().unwrap_or_default())
758 .unwrap_or_default();
759
760 let duration = start.elapsed();
761 let exit_code = status.code().unwrap_or(-1);
762
763 if let Some(ref bridge) = bridge {
765 if exit_code == 0 {
766 bridge.complete(serde_json::json!({
767 "exit_code": exit_code,
768 "duration_ms": duration.as_millis()
769 }));
770 } else {
771 bridge.fail(&format!("Command exited with code {}", exit_code));
772 }
773 }
774
775 Ok(CommandOutput {
776 exit_code,
777 stdout: stdout_output,
778 stderr: stderr_output,
779 duration,
780 })
781 }
782
783 pub fn spawn(mut self) -> Result<WrappedChild> {
785 let bridge = CliBridge::connect_with_config(self.bridge_config.clone()).ok();
787
788 let task_id = if let Some(ref bridge) = bridge {
790 bridge.register_task(&self.task_name, &self.task_type).ok()
791 } else {
792 None
793 };
794
795 let child = self.command.spawn().map_err(IpcError::Io)?;
797
798 Ok(WrappedChild {
799 child,
800 bridge,
801 task_id,
802 start_time: Instant::now(),
803 })
804 }
805}
806
807pub struct WrappedChild {
809 child: Child,
810 bridge: Option<CliBridge>,
811 task_id: Option<String>,
812 start_time: Instant,
813}
814
815impl WrappedChild {
816 pub fn wait(mut self) -> Result<CommandOutput> {
818 let status = self.child.wait().map_err(IpcError::Io)?;
819 let duration = self.start_time.elapsed();
820 let exit_code = status.code().unwrap_or(-1);
821
822 if let Some(ref bridge) = self.bridge {
824 if exit_code == 0 {
825 bridge.complete(serde_json::json!({
826 "exit_code": exit_code,
827 "duration_ms": duration.as_millis()
828 }));
829 } else {
830 bridge.fail(&format!("Command exited with code {}", exit_code));
831 }
832 }
833
834 Ok(CommandOutput {
835 exit_code,
836 stdout: String::new(), stderr: String::new(),
838 duration,
839 })
840 }
841
842 pub fn cancel(&mut self) -> Result<()> {
844 self.child.kill().map_err(IpcError::Io)
845 }
846
847 pub fn task_id(&self) -> Option<&str> {
849 self.task_id.as_deref()
850 }
851
852 pub fn try_wait(&mut self) -> Result<Option<ExitStatus>> {
854 self.child.try_wait().map_err(IpcError::Io)
855 }
856}
857
858#[cfg(test)]
859mod tests {
860 use super::*;
861
862 #[test]
865 fn test_percentage_parser() {
866 let parser = parsers::PercentageParser;
867
868 assert_eq!(parser.parse("50%").map(|p| p.percentage()), Some(50));
869 assert_eq!(
870 parser.parse("Progress: 75%").map(|p| p.percentage()),
871 Some(75)
872 );
873 assert_eq!(
874 parser.parse("Downloading... 100%").map(|p| p.percentage()),
875 Some(100)
876 );
877 assert_eq!(
878 parser.parse("No progress here").map(|p| p.percentage()),
879 None
880 );
881 }
882
883 #[test]
884 fn test_percentage_parser_edge_cases() {
885 let parser = parsers::PercentageParser;
886
887 assert_eq!(parser.parse("0%").map(|p| p.percentage()), Some(0));
889 assert_eq!(parser.parse("1%").map(|p| p.percentage()), Some(1));
890 assert_eq!(parser.parse("99%").map(|p| p.percentage()), Some(99));
891
892 assert_eq!(parser.parse("150%").map(|p| p.percentage()), Some(100));
894
895 let info = parser.parse("Step 1: 25% complete, overall: 50%");
897 assert_eq!(info.map(|p| p.percentage()), Some(25));
898 }
899
900 #[test]
901 fn test_fraction_parser() {
902 let parser = parsers::FractionParser;
903
904 let info = parser.parse("5/10").unwrap();
905 assert_eq!(info.current, 5);
906 assert_eq!(info.total, 10);
907 assert_eq!(info.percentage(), 50);
908
909 let info = parser.parse("[3/4] Installing...").unwrap();
910 assert_eq!(info.current, 3);
911 assert_eq!(info.total, 4);
912 assert_eq!(info.percentage(), 75);
913
914 assert!(parser.parse("No fraction").is_none());
915 }
916
917 #[test]
918 fn test_fraction_parser_edge_cases() {
919 let parser = parsers::FractionParser;
920
921 let info = parser.parse("0/10").unwrap();
923 assert_eq!(info.percentage(), 0);
924
925 let info = parser.parse("10/10").unwrap();
926 assert_eq!(info.percentage(), 100);
927
928 let info = parser.parse("5/0").unwrap();
930 assert_eq!(info.percentage(), 0);
931
932 let info = parser.parse("3 / 5").unwrap();
934 assert_eq!(info.current, 3);
935 assert_eq!(info.total, 5);
936
937 let info = parser.parse("999/1000").unwrap();
939 assert_eq!(info.percentage(), 99);
940 }
941
942 #[test]
943 fn test_progress_bar_parser() {
944 let parser = parsers::ProgressBarParser;
945
946 assert_eq!(
947 parser.parse("[=====> ] 50%").map(|p| p.percentage()),
948 Some(50)
949 );
950 assert_eq!(
951 parser.parse("[##########] 100%").map(|p| p.percentage()),
952 Some(100)
953 );
954 }
955
956 #[test]
957 fn test_progress_bar_parser_variants() {
958 let parser = parsers::ProgressBarParser;
959
960 assert_eq!(
962 parser.parse("[----------] 0%").map(|p| p.percentage()),
963 Some(0)
964 );
965 assert_eq!(
966 parser.parse("[###-------] 30%").map(|p| p.percentage()),
967 Some(30)
968 );
969 assert_eq!(
970 parser.parse("[> ] 10%").map(|p| p.percentage()),
971 Some(10)
972 );
973 }
974
975 #[test]
976 fn test_composite_parser() {
977 let parser = parsers::CompositeParser::default_all();
978
979 assert_eq!(parser.parse("50%").map(|p| p.percentage()), Some(50));
980 assert_eq!(parser.parse("5/10").map(|p| p.percentage()), Some(50));
981 assert_eq!(
982 parser.parse("[=====> ] 50%").map(|p| p.percentage()),
983 Some(50)
984 );
985 }
986
987 #[test]
988 fn test_composite_parser_priority() {
989 let parser = parsers::CompositeParser::default_all();
990
991 let info = parser.parse("Step 3/5: 60% complete");
993 assert_eq!(info.map(|p| p.percentage()), Some(60));
994
995 let info = parser.parse("Processing file 3/5");
997 assert_eq!(info.map(|p| p.percentage()), Some(60));
998 }
999
1000 #[test]
1001 fn test_composite_parser_no_match() {
1002 let parser = parsers::CompositeParser::default_all();
1003 assert!(parser.parse("Just some text").is_none());
1004 assert!(parser.parse("").is_none());
1005 }
1006
1007 #[test]
1008 fn test_custom_composite_parser() {
1009 let parser = parsers::CompositeParser::new()
1010 .add(parsers::FractionParser)
1011 .add(parsers::PercentageParser);
1012
1013 let info = parser.parse("Step 3/5: 60% complete");
1015 assert_eq!(info.map(|p| p.percentage()), Some(60)); }
1017
1018 #[test]
1021 fn test_progress_info() {
1022 let info = ProgressInfo::new(50, 100);
1023 assert_eq!(info.percentage(), 50);
1024
1025 let info = ProgressInfo::new(0, 0);
1026 assert_eq!(info.percentage(), 0);
1027
1028 let info = ProgressInfo::with_message(75, 100, "Almost done");
1029 assert_eq!(info.percentage(), 75);
1030 assert_eq!(info.message, Some("Almost done".to_string()));
1031 }
1032
1033 #[test]
1034 fn test_progress_info_edge_cases() {
1035 let info = ProgressInfo::new(50, 0);
1037 assert_eq!(info.percentage(), 0);
1038
1039 let info = ProgressInfo::new(150, 100);
1041 assert_eq!(info.percentage(), 100);
1042
1043 let info = ProgressInfo::new(500000, 1000000);
1045 assert_eq!(info.percentage(), 50);
1046 }
1047
1048 #[test]
1049 fn test_progress_info_serialization() {
1050 let info = ProgressInfo::with_message(50, 100, "Halfway");
1051 let json = serde_json::to_string(&info).unwrap();
1052 assert!(json.contains("50"));
1053 assert!(json.contains("100"));
1054 assert!(json.contains("Halfway"));
1055
1056 let deserialized: ProgressInfo = serde_json::from_str(&json).unwrap();
1057 assert_eq!(deserialized.current, 50);
1058 assert_eq!(deserialized.total, 100);
1059 assert_eq!(deserialized.message, Some("Halfway".to_string()));
1060 }
1061
1062 #[test]
1065 fn test_cli_bridge_config() {
1066 let config = CliBridgeConfig::default();
1067 assert!(config.auto_register);
1068 assert!(config.capture_stdout);
1069 assert!(config.capture_stderr);
1070
1071 let config = CliBridgeConfig::with_server("/tmp/test.sock");
1072 assert_eq!(config.server_url, "/tmp/test.sock");
1073
1074 let config = CliBridgeConfig::default().no_auto_register();
1075 assert!(!config.auto_register);
1076 }
1077
1078 #[test]
1079 fn test_cli_bridge_config_builder() {
1080 let config = CliBridgeConfig::default()
1081 .no_auto_register()
1082 .progress_parser(parsers::PercentageParser);
1083
1084 assert!(!config.auto_register);
1085 assert!(config.progress_parser.is_some());
1086 }
1087
1088 #[test]
1089 fn test_cli_bridge_config_debug() {
1090 let config = CliBridgeConfig::default();
1091 let debug_str = format!("{:?}", config);
1092 assert!(debug_str.contains("CliBridgeConfig"));
1093 assert!(debug_str.contains("auto_register"));
1094 }
1095
1096 #[test]
1099 fn test_cli_bridge_creation() {
1100 let bridge = CliBridge::new(CliBridgeConfig::default()).unwrap();
1101 assert!(bridge.task_id().is_none());
1102 assert!(!bridge.is_cancelled());
1103 }
1104
1105 #[test]
1106 fn test_cli_bridge_register_task() {
1107 let bridge = CliBridge::new(CliBridgeConfig::default()).unwrap();
1108 let task_id = bridge.register_task("Test Task", "test").unwrap();
1109
1110 assert!(task_id.starts_with("cli-"));
1111 assert_eq!(bridge.task_id(), Some(task_id));
1112 }
1113
1114 #[test]
1115 fn test_cli_bridge_progress() {
1116 let bridge = CliBridge::new(CliBridgeConfig::default()).unwrap();
1117 bridge.register_task("Test", "test").unwrap();
1118
1119 bridge.set_progress(50, Some("Halfway"));
1120 let state = bridge.state.read();
1122 assert_eq!(state.progress, 50);
1123 assert_eq!(state.progress_message, Some("Halfway".to_string()));
1124 }
1125
1126 #[test]
1127 fn test_cli_bridge_progress_clamping() {
1128 let bridge = CliBridge::new(CliBridgeConfig::default()).unwrap();
1129 bridge.register_task("Test", "test").unwrap();
1130
1131 bridge.set_progress(150, None);
1133 let state = bridge.state.read();
1134 assert_eq!(state.progress, 100);
1135 }
1136
1137 #[test]
1138 fn test_cli_bridge_cancellation() {
1139 let bridge = CliBridge::new(CliBridgeConfig::default()).unwrap();
1140 assert!(!bridge.is_cancelled());
1141
1142 let token = bridge.cancel_token();
1144 token.cancel();
1145 assert!(bridge.is_cancelled());
1146 }
1147
1148 #[test]
1149 fn test_cli_bridge_complete() {
1150 let bridge = CliBridge::new(CliBridgeConfig::default()).unwrap();
1151 bridge.register_task("Test", "test").unwrap();
1152
1153 bridge.complete(serde_json::json!({"success": true}));
1154
1155 let state = bridge.state.read();
1156 assert!(state.completed.load(std::sync::atomic::Ordering::SeqCst));
1157 }
1158
1159 #[test]
1160 fn test_cli_bridge_fail() {
1161 let bridge = CliBridge::new(CliBridgeConfig::default()).unwrap();
1162 bridge.register_task("Test", "test").unwrap();
1163
1164 bridge.fail("Something went wrong");
1165
1166 let state = bridge.state.read();
1167 assert!(state.completed.load(std::sync::atomic::Ordering::SeqCst));
1168 }
1169
1170 #[test]
1173 fn test_wrapped_command_creation() {
1174 let cmd = WrappedCommand::new("echo")
1175 .arg("hello")
1176 .task("Echo Test", "test");
1177
1178 assert_eq!(cmd.task_name, "Echo Test");
1179 assert_eq!(cmd.task_type, "test");
1180 }
1181
1182 #[test]
1183 fn test_wrapped_command_builder() {
1184 let cmd = WrappedCommand::new("cargo")
1185 .args(["build", "--release"])
1186 .task("Build", "build")
1187 .progress_parser(parsers::PercentageParser);
1188
1189 assert_eq!(cmd.task_name, "Build");
1190 assert_eq!(cmd.task_type, "build");
1191 assert!(cmd.progress_parser.is_some());
1192 }
1193
1194 #[test]
1195 fn test_wrapped_command_env() {
1196 let cmd = WrappedCommand::new("echo")
1197 .env("MY_VAR", "my_value")
1198 .env("ANOTHER_VAR", "another_value");
1199
1200 assert_eq!(cmd.task_type, "command");
1202 }
1203
1204 #[test]
1205 fn test_wrapped_command_current_dir() {
1206 let cmd = WrappedCommand::new("echo").current_dir(std::path::Path::new("/tmp"));
1207
1208 assert_eq!(cmd.task_type, "command");
1209 }
1210
1211 #[cfg(windows)]
1212 #[test]
1213 fn test_wrapped_command_run_echo() {
1214 let output = WrappedCommand::new("cmd")
1215 .args(["/C", "echo", "hello"])
1216 .task("Echo Test", "test")
1217 .run()
1218 .unwrap();
1219
1220 assert_eq!(output.exit_code, 0);
1221 assert!(output.stdout.contains("hello"));
1222 }
1223
1224 #[cfg(not(windows))]
1225 #[test]
1226 fn test_wrapped_command_run_echo() {
1227 let output = WrappedCommand::new("echo")
1228 .arg("hello")
1229 .task("Echo Test", "test")
1230 .run()
1231 .unwrap();
1232
1233 assert_eq!(output.exit_code, 0);
1234 assert!(output.stdout.contains("hello"));
1235 }
1236
1237 #[cfg(windows)]
1238 #[test]
1239 fn test_wrapped_command_run_failure() {
1240 let output = WrappedCommand::new("cmd")
1241 .args(["/C", "exit", "1"])
1242 .task("Fail Test", "test")
1243 .run()
1244 .unwrap();
1245
1246 assert_eq!(output.exit_code, 1);
1247 }
1248
1249 #[cfg(not(windows))]
1250 #[test]
1251 fn test_wrapped_command_run_failure() {
1252 let output = WrappedCommand::new("sh")
1253 .args(["-c", "exit 1"])
1254 .task("Fail Test", "test")
1255 .run()
1256 .unwrap();
1257
1258 assert_eq!(output.exit_code, 1);
1259 }
1260
1261 #[test]
1264 fn test_command_output_debug() {
1265 let output = CommandOutput {
1266 exit_code: 0,
1267 stdout: "hello".to_string(),
1268 stderr: String::new(),
1269 duration: Duration::from_millis(100),
1270 };
1271
1272 let debug_str = format!("{:?}", output);
1273 assert!(debug_str.contains("exit_code"));
1274 assert!(debug_str.contains("0"));
1275 }
1276
1277 #[test]
1280 fn test_wrapped_writer_stdout() {
1281 let state = Arc::new(RwLock::new(BridgeState::default()));
1282 let mut writer = WrappedWriter::new(
1283 "/tmp/test.sock".to_string(),
1284 Some("test-task".to_string()),
1285 OutputType::Stdout,
1286 Some(Arc::new(parsers::PercentageParser)),
1287 Arc::clone(&state),
1288 );
1289
1290 let data = b"Progress: 50%\n";
1292 let written = writer.write(data).unwrap();
1293 assert_eq!(written, data.len());
1294
1295 let s = state.read();
1297 assert_eq!(s.progress, 50);
1298 }
1299
1300 #[test]
1301 fn test_wrapped_writer_stderr() {
1302 let state = Arc::new(RwLock::new(BridgeState::default()));
1303 let mut writer = WrappedWriter::new(
1304 "/tmp/test.sock".to_string(),
1305 Some("test-task".to_string()),
1306 OutputType::Stderr,
1307 None,
1308 Arc::clone(&state),
1309 );
1310
1311 let data = b"Error message\n";
1312 let written = writer.write(data).unwrap();
1313 assert_eq!(written, data.len());
1314 }
1315
1316 #[test]
1317 fn test_wrapped_writer_buffering() {
1318 let state = Arc::new(RwLock::new(BridgeState::default()));
1319 let mut writer = WrappedWriter::new(
1320 "/tmp/test.sock".to_string(),
1321 Some("test-task".to_string()),
1322 OutputType::Stdout,
1323 Some(Arc::new(parsers::PercentageParser)),
1324 Arc::clone(&state),
1325 );
1326
1327 writer.write_all(b"Progress: ").unwrap();
1329 assert_eq!(state.read().progress, 0);
1330
1331 writer.write_all(b"75%\n").unwrap();
1333 assert_eq!(state.read().progress, 75);
1334 }
1335
1336 #[test]
1337 fn test_wrapped_writer_flush() {
1338 let state = Arc::new(RwLock::new(BridgeState::default()));
1339 let mut writer = WrappedWriter::new(
1340 "/tmp/test.sock".to_string(),
1341 Some("test-task".to_string()),
1342 OutputType::Stdout,
1343 Some(Arc::new(parsers::PercentageParser)),
1344 Arc::clone(&state),
1345 );
1346
1347 writer.write_all(b"Progress: 90%").unwrap();
1349 assert_eq!(state.read().progress, 0);
1350
1351 writer.flush().unwrap();
1353 assert_eq!(state.read().progress, 90);
1354 }
1355
1356 #[test]
1359 fn test_output_type_equality() {
1360 assert_eq!(OutputType::Stdout, OutputType::Stdout);
1361 assert_eq!(OutputType::Stderr, OutputType::Stderr);
1362 assert_ne!(OutputType::Stdout, OutputType::Stderr);
1363 }
1364
1365 #[test]
1366 fn test_output_type_debug() {
1367 assert_eq!(format!("{:?}", OutputType::Stdout), "Stdout");
1368 assert_eq!(format!("{:?}", OutputType::Stderr), "Stderr");
1369 }
1370}