1use std::collections::{BTreeMap, HashMap};
7use std::ffi::OsString;
8use std::fs;
9use std::io::Write;
10use std::ops::Deref;
11use std::path::{Path, PathBuf};
12use std::process::{Command, Stdio};
13
14use crate::core::config::env_vars::{
15 get_git_exec_path, get_path_to_git, should_use_separate_command_binary, TEST_GIT,
16 TEST_SEPARATE_COMMAND_BINARIES,
17};
18use crate::git::{GitRunInfo, GitVersion, NonZeroOid, Repo};
19use crate::util::get_sh;
20use color_eyre::Help;
21use eyre::Context;
22use itertools::Itertools;
23use lazy_static::lazy_static;
24use once_cell::sync::OnceCell;
25use regex::{Captures, Regex};
26use tempfile::TempDir;
27use tracing::{instrument, warn};
28
29const DUMMY_NAME: &str = "Testy McTestface";
30const DUMMY_EMAIL: &str = "test@example.com";
31const DUMMY_DATE: &str = "Wed 29 Oct 12:34:56 2020 PDT";
32
33#[derive(Clone, Debug)]
35pub struct Git {
36 pub repo_path: PathBuf,
40
41 pub path_to_git: PathBuf,
44
45 pub git_exec_path: PathBuf,
47}
48
49#[derive(Debug)]
51pub struct GitInitOptions {
52 pub make_initial_commit: bool,
55
56 pub run_branchless_init: bool,
58}
59
60impl Default for GitInitOptions {
61 fn default() -> Self {
62 GitInitOptions {
63 make_initial_commit: true,
64 run_branchless_init: true,
65 }
66 }
67}
68
69#[derive(Debug, Default)]
71pub struct GitRunOptions {
72 pub time: isize,
75
76 pub expected_exit_code: i32,
78
79 pub input: Option<String>,
81
82 pub env: HashMap<String, String>,
84}
85
86impl Git {
87 pub fn new(path_to_git: PathBuf, repo_path: PathBuf, git_exec_path: PathBuf) -> Self {
89 Git {
90 repo_path,
91 path_to_git,
92 git_exec_path,
93 }
94 }
95
96 pub fn preprocess_output(&self, stdout: String) -> eyre::Result<String> {
98 let path_to_git = self
99 .path_to_git
100 .to_str()
101 .ok_or_else(|| eyre::eyre!("Could not convert path to Git to string"))?;
102 let output = stdout.replace(path_to_git, "<git-executable>");
103
104 let repo_path = std::fs::canonicalize(&self.repo_path)?;
107
108 let repo_path = repo_path
109 .to_str()
110 .ok_or_else(|| eyre::eyre!("Could not convert repo path to string"))?;
111 let output = output.replace(repo_path, "<repo-path>");
112
113 lazy_static! {
114 static ref CLEAR_LINE_RE: Regex = Regex::new(r"(^|\n).*(\r|\x1B\[K)").unwrap();
124 }
125 let output = CLEAR_LINE_RE
126 .replace_all(&output, |captures: &Captures| {
127 captures[1].to_string()
129 })
130 .into_owned();
131
132 lazy_static! {
133 static ref WHITESPACE_ONLY_LINE_RE: Regex = Regex::new(r"(?m)^ {4}$").unwrap();
146 }
147 let output = WHITESPACE_ONLY_LINE_RE
148 .replace_all(&output, "")
149 .into_owned();
150
151 Ok(output)
152 }
153
154 pub fn get_path_for_env(&self) -> OsString {
156 let cargo_bin_path = assert_cmd::cargo::cargo_bin("git-branchless");
157 let branchless_path = cargo_bin_path
158 .parent()
159 .expect("Unable to find git-branchless path parent");
160 let bash = get_sh().expect("bash missing?");
161 let bash_path = bash.parent().unwrap();
162 std::env::join_paths(vec![
163 branchless_path.as_os_str(),
165 self.git_exec_path.as_os_str(),
167 bash_path.as_os_str(),
169 ])
170 .expect("joining paths")
171 }
172
173 pub fn get_base_env(&self, time: isize) -> Vec<(OsString, OsString)> {
175 let date: OsString = format!("{DUMMY_DATE} -{time:0>2}").into();
178
179 let git_editor = OsString::from(":");
186
187 let new_path = self.get_path_for_env();
188 let envs = vec![
189 ("GIT_CONFIG_NOSYSTEM", OsString::from("1")),
190 ("GIT_AUTHOR_DATE", date.clone()),
191 ("GIT_COMMITTER_DATE", date),
192 ("GIT_EDITOR", git_editor),
193 ("GIT_EXEC_PATH", self.git_exec_path.as_os_str().into()),
194 ("LC_ALL", "C".into()),
195 ("PATH", new_path),
196 (TEST_GIT, self.path_to_git.as_os_str().into()),
197 (
198 TEST_SEPARATE_COMMAND_BINARIES,
199 std::env::var_os(TEST_SEPARATE_COMMAND_BINARIES).unwrap_or_default(),
200 ),
201 ];
202
203 envs.into_iter()
204 .map(|(key, value)| (OsString::from(key), value))
205 .collect()
206 }
207
208 #[track_caller]
209 #[instrument]
210 fn run_with_options_inner(
211 &self,
212 args: &[&str],
213 options: &GitRunOptions,
214 ) -> eyre::Result<(String, String)> {
215 let GitRunOptions {
216 time,
217 expected_exit_code,
218 input,
219 env,
220 } = options;
221
222 let env: BTreeMap<_, _> = self
223 .get_base_env(*time)
224 .into_iter()
225 .chain(
226 env.iter()
227 .map(|(k, v)| (OsString::from(k), OsString::from(v))),
228 )
229 .collect();
230 let mut command = Command::new(&self.path_to_git);
231 command
232 .current_dir(&self.repo_path)
233 .args(args)
234 .env_clear()
235 .envs(&env);
236
237 let result = if let Some(input) = input {
238 let mut child = command
239 .stdin(Stdio::piped())
240 .stdout(Stdio::piped())
241 .stderr(Stdio::piped())
242 .spawn()?;
243 write!(child.stdin.take().unwrap(), "{}", &input)?;
244 child.wait_with_output().wrap_err_with(|| {
245 format!(
246 "Running git
247 Executable: {:?}
248 Args: {:?}
249 Stdin: {:?}
250 Env: <not shown>",
251 &self.path_to_git, &args, input
252 )
253 })?
254 } else {
255 command.output().wrap_err_with(|| {
256 format!(
257 "Running git
258 Executable: {:?}
259 Args: {:?}
260 Env: <not shown>",
261 &self.path_to_git, &args
262 )
263 })?
264 };
265
266 let exit_code = result
267 .status
268 .code()
269 .expect("Failed to read exit code from Git process");
270 let result = if exit_code != *expected_exit_code {
271 eyre::bail!(
272 "Git command {:?} {:?} exited with unexpected code {} (expected {})
273env:
274{:#?}
275stdout:
276{}
277stderr:
278{}",
279 &self.path_to_git,
280 &args,
281 exit_code,
282 expected_exit_code,
283 &env,
284 &String::from_utf8_lossy(&result.stdout),
285 &String::from_utf8_lossy(&result.stderr),
286 )
287 } else {
288 result
289 };
290 let stdout = String::from_utf8(result.stdout)?;
291 let stdout = self.preprocess_output(stdout)?;
292 let stderr = String::from_utf8(result.stderr)?;
293 let stderr = self.preprocess_output(stderr)?;
294 Ok((stdout, stderr))
295 }
296
297 #[track_caller]
299 pub fn run_with_options<S: AsRef<str> + std::fmt::Debug>(
300 &self,
301 args: &[S],
302 options: &GitRunOptions,
303 ) -> eyre::Result<(String, String)> {
304 self.run_with_options_inner(
305 args.iter().map(|arg| arg.as_ref()).collect_vec().as_slice(),
306 options,
307 )
308 }
309
310 #[track_caller]
312 pub fn run<S: AsRef<str> + std::fmt::Debug>(
313 &self,
314 args: &[S],
315 ) -> eyre::Result<(String, String)> {
316 if let Some(first_arg) = args.first() {
317 if first_arg.as_ref() == "branchless" {
318 eyre::bail!(
319 r#"Refusing to invoke `branchless` via `git.run(&["branchless", ...])`; instead, call `git.branchless(&[...])`"#
320 );
321 }
322 }
323
324 self.run_with_options(args, &Default::default())
325 }
326
327 #[instrument]
329 pub fn smartlog(&self) -> eyre::Result<String> {
330 let (stdout, _stderr) = self.branchless("smartlog", &[])?;
331 Ok(stdout)
332 }
333
334 #[track_caller]
337 #[instrument]
338 pub fn branchless(&self, subcommand: &str, args: &[&str]) -> eyre::Result<(String, String)> {
339 self.branchless_with_options(subcommand, args, &Default::default())
340 }
341
342 #[track_caller]
346 #[instrument]
347 pub fn branchless_with_options(
348 &self,
349 subcommand: &str,
350 args: &[&str],
351 options: &GitRunOptions,
352 ) -> eyre::Result<(String, String)> {
353 let mut git_run_args = Vec::new();
354 if should_use_separate_command_binary(subcommand) {
355 git_run_args.push(format!("branchless-{subcommand}"));
356 } else {
357 git_run_args.push("branchless".to_string());
358 git_run_args.push(subcommand.to_string());
359 }
360 git_run_args.extend(args.iter().map(|arg| arg.to_string()));
361
362 let result = self.run_with_options(&git_run_args, options);
363
364 if !should_use_separate_command_binary(subcommand) {
365 let main_command_exe = assert_cmd::cargo::cargo_bin("git-branchless");
366 let subcommand_exe =
367 assert_cmd::cargo::cargo_bin(format!("git-branchless-{subcommand}"));
368 if main_command_exe.exists() && subcommand_exe.exists() {
369 let main_command_mtime = main_command_exe.metadata()?.modified()?;
370 let subcommand_mtime = subcommand_exe.metadata()?.modified()?;
371 if subcommand_mtime > main_command_mtime {
372 result.suggestion(format!(
373 "\
374The modified time for {main_command_exe:?} was before the modified time for
375{subcommand_exe:?}, which may indicate that you made changes to the subcommand
376without building the main executable. This may cause spurious test failures
377because the main executable code is out of date.
378
379If so, you should either explicitly run: cargo -p git-branchless
380to build the main executable before running this test; or, if it's okay to skip
381building the main executable and test only the subcommand executable, you
382can set the environment variable
383`{TEST_SEPARATE_COMMAND_BINARIES}={subcommand}` to directly invoke it.\
384"
385 ))
386 } else {
387 result
388 }
389 } else {
390 result
391 }
392 } else {
393 result.suggestion(format!(
394 "\
395If you have set the {TEST_SEPARATE_COMMAND_BINARIES} environment variable, then \
396the git-branchless-{subcommand} binary is NOT automatically built or updated when \
397running integration tests for other binaries (see \
398https://github.com/rust-lang/cargo/issues/4316 for more details).
399
400Make sure that git-branchless-{subcommand} has been built before running \
401integration tests. You can build it with: cargo build -p git-branchless-{subcommand}
402
403If you have not set the {TEST_SEPARATE_COMMAND_BINARIES} environment variable, \
404then you can only run tests in the main `git-branchless` and \
405`git-branchless-lib` crates.\
406 ",
407 ))
408 }
409 }
410
411 #[instrument]
414 pub fn init_repo_with_options(&self, options: &GitInitOptions) -> eyre::Result<()> {
415 self.run(&["init"])?;
416 self.run(&["config", "user.name", DUMMY_NAME])?;
417 self.run(&["config", "user.email", DUMMY_EMAIL])?;
418 self.run(&["config", "core.abbrev", "7"])?;
419
420 if options.make_initial_commit {
421 self.commit_file("initial", 0)?;
422 }
423
424 self.run(&[
426 "config",
427 "branchless.commitDescriptors.relativeTime",
428 "false",
429 ])?;
430 self.run(&["config", "branchless.restack.preserveTimestamps", "true"])?;
431
432 self.run(&["config", "core.autocrlf", "false"])?;
439
440 if options.run_branchless_init {
441 self.branchless("init", &[])?;
442 }
443
444 Ok(())
445 }
446
447 pub fn init_repo(&self) -> eyre::Result<()> {
450 self.init_repo_with_options(&Default::default())
451 }
452
453 pub fn clone_repo_into(&self, target: &Git, additional_args: &[&str]) -> eyre::Result<()> {
456 let remote = format!("file://{}", self.repo_path.to_str().unwrap());
457 let args = {
458 let mut args = vec![
459 "clone",
460 "-c",
462 "core.autocrlf=false",
463 &remote,
464 target.repo_path.to_str().unwrap(),
465 ];
466 args.extend(additional_args.iter());
467 args
468 };
469
470 let (_stdout, _stderr) = self.run(args.as_slice())?;
471
472 let new_repo = Git {
475 repo_path: target.repo_path.clone(),
476 ..self.clone()
477 };
478 new_repo.init_repo_with_options(&GitInitOptions {
479 make_initial_commit: false,
480 run_branchless_init: false,
481 })?;
482
483 Ok(())
484 }
485
486 pub fn write_file_txt(&self, name: &str, contents: &str) -> eyre::Result<()> {
490 let name = format!("{name}.txt");
491 self.write_file(&name, contents)
492 }
493
494 pub fn write_file(&self, name: &str, contents: &str) -> eyre::Result<()> {
496 let path = self.repo_path.join(name);
497 if let Some(dir) = path.parent() {
498 std::fs::create_dir_all(self.repo_path.join(dir))?;
499 }
500 std::fs::write(&path, contents)?;
501 Ok(())
502 }
503
504 pub fn delete_file(&self, name: &str) -> eyre::Result<()> {
506 let file_path = self.repo_path.join(format!("{name}.txt"));
507 fs::remove_file(file_path)?;
508 Ok(())
509 }
510
511 pub fn set_file_permissions(
513 &self,
514 name: &str,
515 permissions: fs::Permissions,
516 ) -> eyre::Result<()> {
517 let file_path = self.repo_path.join(format!("{name}.txt"));
518 fs::set_permissions(file_path, permissions)?;
519 Ok(())
520 }
521
522 #[instrument]
525 pub fn get_trimmed_diff(&self, file: &str, commit: &str) -> eyre::Result<String> {
526 let (stdout, _stderr) = self.run(&["show", "--pretty=format:", commit])?;
527 let split_on = format!("+++ b/{file}\n");
528 match stdout.as_str().split_once(split_on.as_str()) {
529 Some((_, diff)) => Ok(diff.to_string()),
530 None => eyre::bail!("Error trimming diff. Could not split on '{split_on}'"),
531 }
532 }
533
534 #[track_caller]
538 #[instrument]
539 pub fn commit_file_with_contents_and_message(
540 &self,
541 name: &str,
542 time: isize,
543 contents: &str,
544 message_prefix: &str,
545 ) -> eyre::Result<NonZeroOid> {
546 let message = format!("{message_prefix} {name}.txt");
547 self.write_file_txt(name, contents)?;
548 self.run(&["add", "."])?;
549 self.run_with_options(
550 &["commit", "-m", &message],
551 &GitRunOptions {
552 time,
553 ..Default::default()
554 },
555 )?;
556
557 let repo = self.get_repo()?;
558 let oid = repo
559 .get_head_info()?
560 .oid
561 .expect("Could not find OID for just-created commit");
562 Ok(oid)
563 }
564
565 #[track_caller]
569 #[instrument]
570 pub fn commit_file_with_contents(
571 &self,
572 name: &str,
573 time: isize,
574 contents: &str,
575 ) -> eyre::Result<NonZeroOid> {
576 self.commit_file_with_contents_and_message(name, time, contents, "create")
577 }
578
579 #[track_caller]
582 #[instrument]
583 pub fn commit_file(&self, name: &str, time: isize) -> eyre::Result<NonZeroOid> {
584 self.commit_file_with_contents(name, time, &format!("{name} contents\n"))
585 }
586
587 #[instrument]
591 pub fn detach_head(&self) -> eyre::Result<()> {
592 self.run(&["checkout", "--detach"])?;
593 Ok(())
594 }
595
596 #[instrument]
598 pub fn get_repo(&self) -> eyre::Result<Repo> {
599 let repo = Repo::from_dir(&self.repo_path)?;
600 Ok(repo)
601 }
602
603 #[instrument]
605 pub fn get_version(&self) -> eyre::Result<GitVersion> {
606 let (version_str, _stderr) = self.run(&["version"])?;
607 let version = version_str.parse()?;
608 Ok(version)
609 }
610
611 #[instrument]
613 pub fn get_git_run_info(&self) -> GitRunInfo {
614 GitRunInfo {
615 path_to_git: self.path_to_git.clone(),
616 working_directory: self.repo_path.clone(),
617 env: self.get_base_env(0).into_iter().collect(),
618 }
619 }
620
621 #[instrument]
624 pub fn supports_reference_transactions(&self) -> eyre::Result<bool> {
625 let version = self.get_version()?;
626 Ok(version >= GitVersion(2, 29, 0))
627 }
628
629 pub fn supports_committer_date_is_author_date(&self) -> eyre::Result<bool> {
635 let version = self.get_version()?;
652 Ok(version >= GitVersion(2, 29, 0))
653 }
654
655 pub fn supports_log_exclude_decoration(&self) -> eyre::Result<bool> {
658 let version = self.get_version()?;
659 Ok(version >= GitVersion(2, 27, 0))
660 }
661
662 pub fn produces_auto_merge_refs(&self) -> eyre::Result<bool> {
665 let version = self.get_version()?;
666 Ok(version >= GitVersion(2, 44, 0))
667 }
668
669 #[instrument]
672 pub fn resolve_file(&self, name: &str, contents: &str) -> eyre::Result<()> {
673 let file_path = self.repo_path.join(format!("{name}.txt"));
674 std::fs::write(&file_path, contents)?;
675 let file_path = match file_path.to_str() {
676 None => eyre::bail!("Could not convert file path to string: {:?}", file_path),
677 Some(file_path) => file_path,
678 };
679 self.run(&["add", file_path])?;
680 Ok(())
681 }
682
683 #[instrument]
687 pub fn clear_event_log(&self) -> eyre::Result<()> {
688 let event_log_path = self.repo_path.join(".git/branchless/db.sqlite3");
689 std::fs::remove_file(event_log_path)?;
690 Ok(())
691 }
692}
693
694pub struct GitWrapper {
696 repo_dir: TempDir,
697 git: Git,
698}
699
700impl Deref for GitWrapper {
701 type Target = Git;
702
703 fn deref(&self) -> &Self::Target {
704 &self.git
705 }
706}
707
708fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> std::io::Result<()> {
711 fs::create_dir_all(&dst)?;
712 for entry in fs::read_dir(src)? {
713 let entry = entry?;
714 let ty = entry.file_type()?;
715 if ty.is_dir() {
716 copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
717 } else {
718 fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
719 }
720 }
721 Ok(())
722}
723
724impl GitWrapper {
725 pub fn duplicate_repo(&self) -> eyre::Result<Self> {
731 let repo_dir = tempfile::tempdir()?;
732 copy_dir_all(&self.repo_dir, &repo_dir)?;
733 let git = Git {
734 repo_path: repo_dir.path().to_path_buf(),
735 ..self.git.clone()
736 };
737 Ok(Self { repo_dir, git })
738 }
739}
740
741static COLOR_EYRE_INSTALL: OnceCell<()> = OnceCell::new();
742
743pub fn make_git() -> eyre::Result<GitWrapper> {
745 COLOR_EYRE_INSTALL.get_or_try_init(color_eyre::install)?;
746
747 let repo_dir = tempfile::tempdir()?;
748 let path_to_git = get_path_to_git()?;
749 let git_exec_path = get_git_exec_path()?;
750 let git = Git::new(path_to_git, repo_dir.path().to_path_buf(), git_exec_path);
751 Ok(GitWrapper { repo_dir, git })
752}
753
754pub struct GitWrapperWithRemoteRepo {
758 pub temp_dir: TempDir,
761
762 pub original_repo: Git,
764
765 pub cloned_repo: Git,
767}
768
769pub fn make_git_with_remote_repo() -> eyre::Result<GitWrapperWithRemoteRepo> {
771 let path_to_git = get_path_to_git()?;
772 let git_exec_path = get_git_exec_path()?;
773 let temp_dir = tempfile::tempdir()?;
774 let original_repo_path = temp_dir.path().join("original");
775 std::fs::create_dir_all(&original_repo_path)?;
776 let original_repo = Git::new(
777 path_to_git.clone(),
778 original_repo_path,
779 git_exec_path.clone(),
780 );
781 let cloned_repo_path = temp_dir.path().join("cloned");
782 let cloned_repo = Git::new(path_to_git, cloned_repo_path, git_exec_path);
783
784 Ok(GitWrapperWithRemoteRepo {
785 temp_dir,
786 original_repo,
787 cloned_repo,
788 })
789}
790
791pub struct GitWorktreeWrapper {
793 pub temp_dir: TempDir,
796
797 pub worktree: Git,
799}
800
801pub fn make_git_worktree(git: &Git, worktree_name: &str) -> eyre::Result<GitWorktreeWrapper> {
803 let temp_dir = tempfile::tempdir()?;
804 let worktree_path = temp_dir.path().join(worktree_name);
805 git.run(&[
806 "worktree",
807 "add",
808 "--detach",
809 worktree_path.to_str().unwrap(),
810 ])?;
811 let worktree = Git {
812 repo_path: worktree_path,
813 ..git.clone()
814 };
815 Ok(GitWorktreeWrapper { temp_dir, worktree })
816}
817
818pub fn extract_hint_command(stdout: &str) -> Vec<String> {
821 let hint_command = stdout
822 .split_once("disable this hint by running: ")
823 .map(|(_first, second)| second)
824 .unwrap()
825 .split('\n')
826 .next()
827 .unwrap();
828 hint_command
829 .split(' ')
830 .skip(1) .filter(|s| s != &"--global")
832 .map(|s| s.to_owned())
833 .collect_vec()
834}
835
836pub fn remove_rebase_lines(output: String) -> String {
839 output
840 .lines()
841 .filter(|line| !line.contains("First, rewinding head") && !line.contains("Applying:"))
842 .filter(|line| {
843 !line.contains("Auto-merging")
852 && !line.contains("use \"git pull\"")
856 })
857 .flat_map(|line| [line, "\n"])
858 .collect()
859}
860
861pub fn trim_lines(output: String) -> String {
863 output
864 .lines()
865 .flat_map(|line| vec![line.trim_end(), "\n"].into_iter())
866 .collect()
867}
868
869pub fn remove_nondeterministic_lines(output: String) -> String {
871 output
872 .lines()
873 .filter(|line| {
874 !line.contains("Fetching")
876 && !line.contains("Your branch is up to date")
878 && !line.contains("Switched to branch")
882 && !line.contains("hint:")
884 && !line.is_empty()
889 })
890 .flat_map(|line| [line, "\n"])
891 .collect()
892}
893
894pub mod pty {
896 use std::sync::{mpsc::channel, Arc, Mutex};
897 use std::thread;
898 use std::time::Duration;
899
900 use eyre::eyre;
901 use portable_pty::{native_pty_system, CommandBuilder, ExitStatus, PtySize};
902
903 use super::Git;
904
905 pub const UP_ARROW: &str = "\x1b[A";
907
908 pub const DOWN_ARROW: &str = "\x1b[B";
910
911 pub enum PtyAction<'a> {
913 Write(&'a str),
915
916 WaitUntilContains(&'a str),
919 }
920
921 #[track_caller]
923 pub fn run_in_pty(
924 git: &Git,
925 branchless_subcommand: &str,
926 args: &[&str],
927 inputs: &[PtyAction],
928 ) -> eyre::Result<ExitStatus> {
929 let pty_system = native_pty_system();
931 let pty_size = PtySize::default();
932 let pty = pty_system
933 .openpty(pty_size)
934 .map_err(|e| eyre!("Could not open pty: {}", e))?;
935 let mut pty_master = pty
936 .master
937 .take_writer()
938 .map_err(|e| eyre!("Could not take PTY master writer: {e}"))?;
939
940 let mut cmd = CommandBuilder::new(&git.path_to_git);
942 cmd.env_clear();
943 for (k, v) in git.get_base_env(0) {
944 cmd.env(k, v);
945 }
946 cmd.env("TERM", "xterm");
947 cmd.arg("branchless");
948 cmd.arg(branchless_subcommand);
949 cmd.args(args);
950 cmd.cwd(&git.repo_path);
951
952 let mut child = pty
953 .slave
954 .spawn_command(cmd)
955 .map_err(|e| eyre!("Could not spawn child: {}", e))?;
956
957 let reader = pty
958 .master
959 .try_clone_reader()
960 .map_err(|e| eyre!("Could not clone reader: {}", e))?;
961 let reader = Arc::new(Mutex::new(reader));
962
963 let parser = vt100::Parser::new(pty_size.rows, pty_size.cols, 0);
964 let parser = Arc::new(Mutex::new(parser));
965
966 for action in inputs {
967 match action {
968 PtyAction::WaitUntilContains(value) => {
969 let (finished_tx, finished_rx) = channel();
970
971 let wait_thread = {
972 let parser = Arc::clone(&parser);
973 let reader = Arc::clone(&reader);
974 let value = value.to_string();
975 thread::spawn(move || -> anyhow::Result<()> {
976 loop {
977 {
981 let parser = parser.lock().unwrap();
982 if parser.screen().contents().contains(&value) {
983 break;
984 }
985 }
986
987 let mut reader = reader.lock().unwrap();
988 const BUF_SIZE: usize = 4096;
989 let mut buffer = [0; BUF_SIZE];
990 let n = reader.read(&mut buffer)?;
991 assert!(n < BUF_SIZE, "filled up PTY buffer by reading {n} bytes",);
992
993 {
994 let mut parser = parser.lock().unwrap();
995 parser.process(&buffer[..n]);
996 }
997 }
998
999 finished_tx.send(()).unwrap();
1000 Ok(())
1001 })
1002 };
1003
1004 if finished_rx.recv_timeout(Duration::from_secs(5)).is_err() {
1005 panic!(
1006 "\
1007Timed out waiting for virtual terminal to show string: {:?}
1008Screen contents:
1009-----
1010{}
1011-----
1012",
1013 value,
1014 parser.lock().unwrap().screen().contents(),
1015 );
1016 }
1017
1018 wait_thread.join().unwrap().unwrap();
1019 }
1020
1021 PtyAction::Write(value) => {
1022 if let Ok(Some(exit_status)) = child.try_wait() {
1023 panic!(
1024 "\
1025Tried to write {value:?} to PTY, but the process has already exited with status {exit_status:?}. Screen contents:
1026-----
1027{}
1028-----
1029", parser.lock().unwrap().screen().contents(),
1030 );
1031 }
1032 write!(pty_master, "{value}")?;
1033 pty_master.flush()?;
1034 }
1035 }
1036 }
1037
1038 let read_remainder_of_pty_output_thread = thread::spawn({
1039 let reader = Arc::clone(&reader);
1040 move || {
1041 let mut reader = reader.lock().unwrap();
1042 let mut buffer = Vec::new();
1043 reader.read_to_end(&mut buffer).expect("finish reading pty");
1044 String::from_utf8(buffer).unwrap()
1045 }
1046 });
1047 let exit_status = child.wait()?;
1048
1049 let _ = read_remainder_of_pty_output_thread;
1050 Ok(exit_status)
1059 }
1060}