Skip to main content

p4cli_20251/
lib.rs

1use std::io::{BufReader, Read, Write};
2use std::path::PathBuf;
3use std::process::{Child, Command, ExitStatus, Stdio};
4use std::thread;
5use std::time::Duration;
6
7/// A P4 CLI wrapper that extracts the embedded p4 binary to an isolated temporary directory.
8///
9/// Each `P4Cli` instance gets its own temp directory, eliminating cross-process races.
10/// The temp directory is cleaned up on `Drop`.
11pub struct P4Cli {
12    bin_path: PathBuf,
13    _temp_dir: PathBuf,
14}
15
16/// Collected output from a single `p4` invocation.
17///
18/// Holds raw stdout/stderr bytes (supports binary content) and the exit code.
19/// The child process is guaranteed to have been reaped before this struct is returned.
20pub struct P4Output {
21    exit_code: i32,
22    stdout: Vec<u8>,
23    stderr: Vec<u8>,
24}
25
26impl P4Output {
27    pub fn exit_code(&self) -> i32 {
28        self.exit_code
29    }
30
31    /// Returns `true` if the exit code is `0`.
32    pub fn success(&self) -> bool {
33        self.exit_code == 0
34    }
35
36    /// Raw stdout bytes (may be binary).
37    pub fn stdout(&self) -> &[u8] {
38        &self.stdout
39    }
40
41    /// Raw stderr bytes (may be binary).
42    pub fn stderr(&self) -> &[u8] {
43        &self.stderr
44    }
45
46    /// Decode stdout as UTF-8.
47    pub fn stdout_str(&self) -> Result<&str, std::str::Utf8Error> {
48        std::str::from_utf8(&self.stdout)
49    }
50
51    /// Decode stderr as UTF-8.
52    pub fn stderr_str(&self) -> Result<&str, std::str::Utf8Error> {
53        std::str::from_utf8(&self.stderr)
54    }
55
56    /// Lines of stdout (UTF-8 text only).
57    pub fn stdout_lines(&self) -> Result<Vec<&str>, std::str::Utf8Error> {
58        let s = self.stdout_str()?;
59        if s.is_empty() {
60            Ok(Vec::new())
61        } else {
62            Ok(s.lines().collect())
63        }
64    }
65
66    /// Lines of stderr (UTF-8 text only).
67    pub fn stderr_lines(&self) -> Result<Vec<&str>, std::str::Utf8Error> {
68        let s = self.stderr_str()?;
69        if s.is_empty() {
70            Ok(Vec::new())
71        } else {
72            Ok(s.lines().collect())
73        }
74    }
75}
76
77impl std::fmt::Debug for P4Output {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("P4Output")
80            .field("exit_code", &self.exit_code)
81            .field("stdout_len", &self.stdout.len())
82            .field("stderr_len", &self.stderr.len())
83            .finish()
84    }
85}
86
87// ---------------------------------------------------------------------------
88// Streaming API
89// ---------------------------------------------------------------------------
90
91/// A single event yielded by [`P4Stream`].
92pub enum P4StreamEvent {
93    /// A chunk of stdout bytes (may be partial line / binary).
94    Stdout(Vec<u8>),
95    /// A chunk of stderr bytes (may be partial line / binary).
96    Stderr(Vec<u8>),
97    /// The process has exited with the given code.
98    Exit(i32),
99}
100
101impl P4StreamEvent {
102    /// Try to decode this event's payload as UTF-8. Returns `None` for
103    /// [`Exit`](P4StreamEvent::Exit) or non-UTF-8 data.
104    pub fn as_utf8(&self) -> Option<&str> {
105        match self {
106            P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
107                std::str::from_utf8(data).ok()
108            }
109            P4StreamEvent::Exit(_) => None,
110        }
111    }
112}
113
114impl std::fmt::Display for P4StreamEvent {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self {
117            P4StreamEvent::Stdout(data) | P4StreamEvent::Stderr(data) => {
118                if let Ok(text) = std::str::from_utf8(data) {
119                    write!(f, "{text}")
120                } else {
121                    write!(f, "<{} bytes>", data.len())
122                }
123            }
124            P4StreamEvent::Exit(code) => write!(f, "(exit {code})"),
125        }
126    }
127}
128
129/// A streaming iterator over the output of a `p4` process.
130///
131/// Stdout and stderr are read concurrently by two OS threads as raw byte
132/// chunks (~64 KB each) and merged into a single stream. The process is
133/// automatically reaped when the stream is exhausted. Dropping the stream
134/// mid-way kills the process and cleans up all resources.
135///
136/// The final item yielded is always [`P4StreamEvent::Exit`] with the exit
137/// code, unless the stream is dropped early.
138///
139/// # Encoding
140///
141/// Raw chunks are yielded — the caller decides the encoding.
142/// Use [`P4StreamEvent::as_utf8`] for a quick UTF-8 check, or
143/// `String::from_utf8()` / `encoding_rs` for full control.
144pub struct P4Stream {
145    rx: std::sync::mpsc::Receiver<std::io::Result<P4StreamEvent>>,
146    child: Option<Child>,
147    /// Killing the child closes the pipes, which unblocks the reader
148    /// threads and lets them exit naturally.
149    #[allow(dead_code)]
150    handles: Vec<thread::JoinHandle<()>>,
151    exhausted: bool,
152}
153
154impl Iterator for P4Stream {
155    type Item = std::io::Result<P4StreamEvent>;
156
157    fn next(&mut self) -> Option<Self::Item> {
158        if self.exhausted {
159            return None;
160        }
161        match self.rx.recv() {
162            Ok(item) => Some(item),
163            Err(_) => {
164                // All senders (reader threads) have finished → reap child.
165                self.exhausted = true;
166                let code = self
167                    .child
168                    .take()
169                    .and_then(|mut c| c.wait().ok())
170                    .and_then(|s| s.code())
171                    .unwrap_or(-1);
172                Some(Ok(P4StreamEvent::Exit(code)))
173            }
174        }
175    }
176}
177
178impl Drop for P4Stream {
179    fn drop(&mut self) {
180        // Kill child if still owned (stream was dropped mid-way).
181        if let Some(ref mut child) = self.child {
182            let _ = child.kill();
183            let _ = child.wait();
184        }
185        // The reader threads will now hit EOF and exit on their own.
186    }
187}
188
189// ---------------------------------------------------------------------------
190// Temporary-directory helpers
191// ---------------------------------------------------------------------------
192
193/// Decompress the embedded zstd payload and write it to a fresh per-instance
194/// directory. No persistent cache — each instance gets its own isolated copy.
195fn write_p4_cli_to_disk() -> std::io::Result<(PathBuf, PathBuf)> {
196    let zst_data = get_p4_cli_zst();
197    let binary_data = decompress_zst(&zst_data)?;
198
199    let base = std::env::temp_dir().join("p4cli-20251");
200    std::fs::create_dir_all(&base)?;
201
202    let dir = base.join(format!(
203        "{}_{}",
204        std::process::id(),
205        std::time::SystemTime::now()
206            .duration_since(std::time::UNIX_EPOCH)
207            .map(|d| d.as_nanos())
208            .unwrap_or(0)
209    ));
210    std::fs::create_dir(&dir)?;
211
212    let bin_path = dir.join("p4_binary");
213    let tmp_path = dir.join(".tmp");
214
215    // Atomic write: write to .tmp first, then rename.
216    {
217        let mut file = std::fs::File::create(&tmp_path)?;
218        file.write_all(&binary_data)?;
219        file.sync_all()?;
220    }
221    std::fs::rename(&tmp_path, &bin_path)?;
222    set_executable_perms(&bin_path)?;
223
224    Ok((bin_path, dir))
225}
226
227fn decompress_zst(zst_data: &[u8]) -> std::io::Result<Vec<u8>> {
228    let mut decoder = zstd::stream::Decoder::new(zst_data)?;
229    let mut buf = Vec::new();
230    std::io::copy(&mut decoder, &mut buf)?;
231    Ok(buf)
232}
233
234#[cfg(unix)]
235fn set_executable_perms(path: &std::path::Path) -> std::io::Result<()> {
236    use std::os::unix::fs::PermissionsExt;
237    std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700))
238}
239
240#[cfg(not(unix))]
241fn set_executable_perms(_path: &std::path::Path) -> std::io::Result<()> {
242    Ok(())
243}
244
245// ---------------------------------------------------------------------------
246// Platform-specific binary accessors
247// ---------------------------------------------------------------------------
248
249fn get_p4_cli_zst() -> Vec<u8> {
250    #[cfg(all(target_os = "windows", target_arch = "x86_64"))]
251    {
252        use p4cli_20251_win_x64::get_p4_cli_zst;
253        get_p4_cli_zst()
254    }
255
256    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
257    {
258        use p4cli_20251_mac_arm64::get_p4_cli_zst;
259        get_p4_cli_zst()
260    }
261
262    #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
263    {
264        use p4cli_20251_mac_x64::get_p4_cli_zst;
265        get_p4_cli_zst()
266    }
267
268    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
269    {
270        use p4cli_20251_linux_x64::get_p4_cli_zst;
271        get_p4_cli_zst()
272    }
273
274    #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
275    {
276        use p4cli_20251_linux_arm64::get_p4_cli_zst;
277        get_p4_cli_zst()
278    }
279
280    #[cfg(not(any(
281        all(target_os = "windows", target_arch = "x86_64"),
282        all(target_os = "macos", target_arch = "aarch64"),
283        all(target_os = "macos", target_arch = "x86_64"),
284        all(target_os = "linux", target_arch = "x86_64"),
285        all(target_os = "linux", target_arch = "aarch64")
286    )))]
287    {
288        compile_error!(format!(
289            "Unsupported platform: {}-{}",
290            std::env::consts::OS,
291            std::env::consts::ARCH
292        ));
293        Vec::new()
294    }
295}
296
297// ---------------------------------------------------------------------------
298// Builder for a single p4 invocation
299// ---------------------------------------------------------------------------
300
301/// Builder-style interface for running a single `p4` command.
302///
303/// Obtain one via [`P4Cli::command()`] and chain configuration calls
304/// before calling [`run`](P4Command::run).
305pub struct P4Command<'a> {
306    cli: &'a P4Cli,
307    args: Vec<std::ffi::OsString>,
308    timeout: Option<Duration>,
309    cwd: Option<PathBuf>,
310    envs: Vec<(std::ffi::OsString, std::ffi::OsString)>,
311    stdin_data: Option<Vec<u8>>,
312}
313
314impl<'a> P4Command<'a> {
315    fn new(cli: &'a P4Cli) -> Self {
316        Self {
317            cli,
318            args: Vec::new(),
319            timeout: None,
320            cwd: None,
321            envs: Vec::new(),
322            stdin_data: None,
323        }
324    }
325
326    /// Append a single argument.
327    pub fn arg(&mut self, arg: impl AsRef<std::ffi::OsStr>) -> &mut Self {
328        self.args.push(arg.as_ref().to_os_string());
329        self
330    }
331
332    /// Append all arguments from a slice.
333    pub fn args(&mut self, args: &[impl AsRef<std::ffi::OsStr>]) -> &mut Self {
334        self.args
335            .extend(args.iter().map(|a| a.as_ref().to_os_string()));
336        self
337    }
338
339    /// Maximum wall-clock time the process is allowed to run.
340    /// When exceeded the process is killed.
341    ///
342    /// **Note**: Only the direct child process is terminated,
343    /// not its descendants (no process-tree kill).
344    pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
345        self.timeout = Some(timeout);
346        self
347    }
348
349    /// Working directory for the child process.
350    pub fn cwd(&mut self, path: impl Into<PathBuf>) -> &mut Self {
351        self.cwd = Some(path.into());
352        self
353    }
354
355    /// Set an environment variable for the child process.
356    pub fn env(
357        &mut self,
358        key: impl Into<std::ffi::OsString>,
359        val: impl Into<std::ffi::OsString>,
360    ) -> &mut Self {
361        self.envs.push((key.into(), val.into()));
362        self
363    }
364
365    /// Provide data to be piped to the child's stdin.
366    pub fn stdin(&mut self, data: impl Into<Vec<u8>>) -> &mut Self {
367        self.stdin_data = Some(data.into());
368        self
369    }
370
371    /// Execute the command and collect output.
372    ///
373    /// Stdout and stderr are read concurrently in separate OS threads to
374    /// prevent pipe-full deadlocks. If a [`timeout`](Self::timeout) was set,
375    /// the process is killed once the deadline is reached.
376    pub fn run(&mut self) -> std::io::Result<P4Output> {
377        let mut cmd = Command::new(&self.cli.bin_path);
378        cmd.args(&self.args)
379            .stdout(Stdio::piped())
380            .stderr(Stdio::piped());
381
382        if self.stdin_data.is_some() {
383            cmd.stdin(Stdio::piped());
384        } else {
385            cmd.stdin(Stdio::null());
386        }
387
388        if let Some(ref cwd) = self.cwd {
389            cmd.current_dir(cwd);
390        }
391        for (k, v) in &self.envs {
392            cmd.env(k, v);
393        }
394
395        let mut child = cmd.spawn()?;
396
397        // Write stdin in a background thread if data was provided.
398        if let Some(data) = self.stdin_data.take()
399            && let Some(mut stdin) = child.stdin.take()
400        {
401            thread::spawn(move || {
402                let _ = stdin.write_all(&data);
403            });
404        }
405
406        let stdout = child
407            .stdout
408            .take()
409            .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
410        let stderr = child
411            .stderr
412            .take()
413            .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
414
415        // Read stdout and stderr concurrently in dedicated threads.
416        let stdout_handle = thread::spawn(move || {
417            let mut buf = Vec::new();
418            BufReader::new(stdout).read_to_end(&mut buf)?;
419            Ok::<_, std::io::Error>(buf)
420        });
421
422        let stderr_handle = thread::spawn(move || {
423            let mut buf = Vec::new();
424            BufReader::new(stderr).read_to_end(&mut buf)?;
425            Ok::<_, std::io::Error>(buf)
426        });
427
428        // Wait for the process (with optional timeout).
429        let exit_status = wait_process(&mut child, self.timeout)?;
430
431        let stdout_buf = stdout_handle
432            .join()
433            .map_err(|_| std::io::Error::other("stdout reader thread panicked"))?
434            .map_err(|e| std::io::Error::other(format!("stdout read failed: {e}")))?;
435        let stderr_buf = stderr_handle
436            .join()
437            .map_err(|_| std::io::Error::other("stderr reader thread panicked"))?
438            .map_err(|e| std::io::Error::other(format!("stderr read failed: {e}")))?;
439
440        Ok(P4Output {
441            exit_code: exit_status.code().unwrap_or(-1),
442            stdout: stdout_buf,
443            stderr: stderr_buf,
444        })
445    }
446
447    /// Run the command and return a streaming iterator over output lines.
448    ///
449    /// Stdout and stderr are read concurrently by two OS threads and merged
450    /// into a single stream. The final event is always
451    /// [`P4StreamEvent::Exit`] with the exit code (unless the stream is
452    /// dropped early).
453    ///
454    /// Unlike [`run`](Self::run), this method does **not** support
455    /// [`timeout`](Self::timeout) — the caller controls iteration and can
456    /// drop the stream to cancel.
457    pub fn stream(&mut self) -> std::io::Result<P4Stream> {
458        let mut cmd = Command::new(&self.cli.bin_path);
459        cmd.args(&self.args)
460            .stdout(Stdio::piped())
461            .stderr(Stdio::piped());
462
463        if self.stdin_data.is_some() {
464            cmd.stdin(Stdio::piped());
465        } else {
466            cmd.stdin(Stdio::null());
467        }
468        if let Some(ref cwd) = self.cwd {
469            cmd.current_dir(cwd);
470        }
471        for (k, v) in &self.envs {
472            cmd.env(k, v);
473        }
474
475        let mut child = cmd.spawn()?;
476
477        if let Some(data) = self.stdin_data.take()
478            && let Some(mut stdin) = child.stdin.take()
479        {
480            thread::spawn(move || {
481                let _ = stdin.write_all(&data);
482            });
483        }
484
485        let mut stdout = child
486            .stdout
487            .take()
488            .ok_or_else(|| std::io::Error::other("stdout was not captured"))?;
489        let mut stderr = child
490            .stderr
491            .take()
492            .ok_or_else(|| std::io::Error::other("stderr was not captured"))?;
493
494        let (tx, rx) = std::sync::mpsc::channel();
495        let mut handles = Vec::new();
496
497        // Stdout reader thread (64 KB chunks).
498        let tx_out = tx.clone();
499        handles.push(thread::spawn(move || {
500            let mut buf = vec![0u8; 65536];
501            loop {
502                let n = match stdout.read(&mut buf) {
503                    Ok(0) => break, // EOF
504                    Ok(n) => n,
505                    Err(e) => {
506                        let _ = tx_out.send(Err(e));
507                        break;
508                    }
509                };
510                if tx_out
511                    .send(Ok(P4StreamEvent::Stdout(buf[..n].to_vec())))
512                    .is_err()
513                {
514                    break;
515                }
516            }
517        }));
518
519        // Stderr reader thread (64 KB chunks).
520        let tx_err = tx.clone();
521        handles.push(thread::spawn(move || {
522            let mut buf = vec![0u8; 65536];
523            loop {
524                let n = match stderr.read(&mut buf) {
525                    Ok(0) => break,
526                    Ok(n) => n,
527                    Err(e) => {
528                        let _ = tx_err.send(Err(e));
529                        break;
530                    }
531                };
532                if tx_err
533                    .send(Ok(P4StreamEvent::Stderr(buf[..n].to_vec())))
534                    .is_err()
535                {
536                    break;
537                }
538            }
539        }));
540
541        Ok(P4Stream {
542            rx,
543            child: Some(child),
544            handles,
545            exhausted: false,
546        })
547    }
548}
549
550/// Block until `child` exits, optionally killing it after `timeout`.
551fn wait_process(child: &mut Child, timeout: Option<Duration>) -> std::io::Result<ExitStatus> {
552    match timeout {
553        None => child.wait(),
554        Some(t) => wait_with_timeout(child, t),
555    }
556}
557
558fn wait_with_timeout(child: &mut Child, timeout: Duration) -> std::io::Result<ExitStatus> {
559    let start = std::time::Instant::now();
560    loop {
561        if let Some(status) = child.try_wait()? {
562            return Ok(status);
563        }
564        if start.elapsed() >= timeout {
565            child.kill()?;
566            return child.wait();
567        }
568        thread::sleep(Duration::from_millis(50));
569    }
570}
571
572// ---------------------------------------------------------------------------
573// Public API
574// ---------------------------------------------------------------------------
575
576impl P4Cli {
577    /// Create a new `P4Cli` instance.
578    ///
579    /// The embedded p4 binary is decompressed and written to an isolated
580    /// temporary directory. Each call performs a fresh decompression;
581    /// there is no persistent cache (see security notes in the crate docs).
582    pub fn new() -> std::io::Result<Self> {
583        let (bin_path, temp_dir) = write_p4_cli_to_disk()?;
584        Ok(Self {
585            bin_path,
586            _temp_dir: temp_dir,
587        })
588    }
589
590    /// Convenience method: run p4 with the given arguments.
591    ///
592    /// This is equivalent to `self.command().args(args).run()`.
593    pub fn run<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Output> {
594        self.command().args(args).run()
595    }
596
597    /// Convenience method: stream p4 output with the given arguments.
598    ///
599    /// This is equivalent to `self.command().args(args).stream()`.
600    pub fn stream<S: AsRef<std::ffi::OsStr>>(&self, args: &[S]) -> std::io::Result<P4Stream> {
601        self.command().args(args).stream()
602    }
603
604    /// Obtain a [`P4Command`] builder for fine-grained control over
605    /// working directory, environment variables, stdin, and timeout.
606    pub fn command(&self) -> P4Command<'_> {
607        P4Command::new(self)
608    }
609}
610
611impl Drop for P4Cli {
612    fn drop(&mut self) {
613        let _ = std::fs::remove_dir_all(&self._temp_dir);
614    }
615}
616
617// ---------------------------------------------------------------------------
618// Tests
619// ---------------------------------------------------------------------------
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624
625    #[test]
626    fn test_run_help() -> std::io::Result<()> {
627        let p4 = P4Cli::new()?;
628        let output = p4.run(&["--help"])?;
629        assert!(output.success(), "p4 --help should exit with 0");
630        let stdout = output
631            .stdout_str()
632            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
633        assert!(
634            stdout.contains("Usage:"),
635            "expected --help output to contain 'Usage:'"
636        );
637        Ok(())
638    }
639
640    #[test]
641    fn test_run_error() -> std::io::Result<()> {
642        let p4 = P4Cli::new()?;
643        let output = p4.run(&["--nonexistent-flag"])?;
644        assert!(!output.success(), "unknown flag should exit non-zero");
645        let stderr = output
646            .stderr_str()
647            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
648        assert!(
649            stderr.contains("Invalid option") || stderr.contains("error"),
650            "expected error output, got: {stderr}"
651        );
652        Ok(())
653    }
654
655    #[test]
656    fn test_multiple_instances() -> std::io::Result<()> {
657        let p4_a = P4Cli::new()?;
658        let p4_b = P4Cli::new()?;
659        assert!(p4_a.run(&["--help"])?.success());
660        assert!(p4_b.run(&["--help"])?.success());
661        Ok(())
662    }
663
664    #[test]
665    fn test_command_builder() -> std::io::Result<()> {
666        let p4 = P4Cli::new()?;
667        let output = p4.command().arg("--help").run()?;
668        assert!(output.success());
669        Ok(())
670    }
671
672    #[test]
673    fn test_timeout_kills() -> std::io::Result<()> {
674        let p4 = P4Cli::new()?;
675        // Run with a very short timeout – should be killed.
676        let output = p4
677            .command()
678            .arg("help")
679            .timeout(Duration::from_millis(1))
680            .run()?;
681        // After kill the exit code is typically non-zero (e.g. -1 or a signal number).
682        // We only verify the call does not hang.
683        assert!(!output.success() || output.exit_code() == 0);
684        Ok(())
685    }
686
687    #[test]
688    fn test_stream_help() -> std::io::Result<()> {
689        let p4 = P4Cli::new()?;
690        let mut saw_stdout = false;
691        let mut saw_exit = false;
692        for event in p4.stream(&["--help"])? {
693            match &event? {
694                P4StreamEvent::Stdout(data) => {
695                    if let Ok(text) = std::str::from_utf8(data)
696                        && text.contains("Usage:")
697                    {
698                        saw_stdout = true;
699                    }
700                }
701                P4StreamEvent::Stderr(_) => {}
702                P4StreamEvent::Exit(code) => {
703                    assert_eq!(*code, 0);
704                    saw_exit = true;
705                }
706            }
707        }
708        assert!(saw_stdout, "expected --help to contain 'Usage:'");
709        assert!(saw_exit, "expected Exit event");
710        Ok(())
711    }
712
713    #[test]
714    fn test_stream_error() -> std::io::Result<()> {
715        let p4 = P4Cli::new()?;
716        let mut saw_stderr = false;
717        let mut saw_exit = false;
718        for event in p4.stream(&["--nonexistent-flag"])? {
719            match &event? {
720                P4StreamEvent::Stdout(_) => {}
721                P4StreamEvent::Stderr(data) => {
722                    if let Ok(text) = std::str::from_utf8(data)
723                        && (text.contains("Invalid option") || text.contains("error"))
724                    {
725                        saw_stderr = true;
726                    }
727                }
728                P4StreamEvent::Exit(code) => {
729                    assert_ne!(*code, 0, "nonexistent flag should fail");
730                    saw_exit = true;
731                }
732            }
733        }
734        assert!(saw_stderr, "expected error output");
735        assert!(saw_exit, "expected Exit event");
736        Ok(())
737    }
738
739    #[test]
740    fn test_stream_drop_midway() -> std::io::Result<()> {
741        // Dropping the stream mid-way must not hang or panic.
742        let p4 = P4Cli::new()?;
743        let stream = p4.stream(&["--help"])?;
744        drop(stream);
745        Ok(())
746    }
747
748    #[test]
749    fn test_stream_builder() -> std::io::Result<()> {
750        let p4 = P4Cli::new()?;
751        let mut saw_exit = false;
752        for event in p4.command().arg("--help").stream()? {
753            if let P4StreamEvent::Exit(code) = event? {
754                assert_eq!(code, 0);
755                saw_exit = true;
756            }
757        }
758        assert!(saw_exit);
759        Ok(())
760    }
761}