trident_client/commander/
mod.rs

1use anyhow::Context;
2use fehler::throw;
3use fehler::throws;
4use std::env;
5use std::io;
6use std::path::Path;
7use std::path::PathBuf;
8use std::process::Stdio;
9use std::string::FromUtf8Error;
10use thiserror::Error;
11use tokio::io::AsyncWriteExt;
12use tokio::process::Child;
13use tokio::process::Command;
14use tokio::signal;
15
16use crate::constants::TESTS_WORKSPACE_DIRECTORY;
17
18mod fuzz;
19
20#[derive(Error, Debug)]
21pub enum Error {
22    #[error("{0:?}")]
23    Io(#[from] io::Error),
24    #[error("{0:?}")]
25    Utf8(#[from] FromUtf8Error),
26    #[error("build programs failed")]
27    BuildProgramsFailed,
28    #[error("fuzzing failed")]
29    FuzzingFailed,
30    #[error("Fuzzing found failing invariants or unhandled panics")]
31    FuzzingFailedInvariantOrPanic,
32    #[error("Coverage error: {0}")]
33    Coverage(#[from] crate::coverage::CoverageError),
34    #[error("Cannot find the trident-tests directory in the current workspace")]
35    BadWorkspace,
36    #[error("{0:?}")]
37    Anyhow(#[from] anyhow::Error),
38}
39
40/// `Commander` allows you to start localnet, build programs,
41/// run tests and do other useful operations.
42#[derive(Default)]
43pub struct Commander {
44    root: PathBuf,
45}
46
47impl Commander {
48    pub fn new(root: &str) -> Self {
49        Self {
50            root: Path::new(&root).to_path_buf(),
51        }
52    }
53
54    #[throws]
55    pub async fn build_anchor_project(root: &Path, program_name: Option<String>) {
56        let mut cmd = Command::new("anchor");
57        cmd.arg("build");
58        cmd.current_dir(root);
59
60        if let Some(name) = program_name {
61            cmd.args(["-p", name.as_str()]);
62        }
63
64        let success = cmd.spawn()?.wait().await?.success();
65        if !success {
66            throw!(Error::BuildProgramsFailed);
67        }
68    }
69
70    /// Formats program code.
71    #[throws]
72    pub async fn format_program_code(code: &str) -> String {
73        let mut rustfmt = Command::new("rustfmt")
74            .args(["--edition", "2018"])
75            .kill_on_drop(true)
76            .stdin(Stdio::piped())
77            .stdout(Stdio::piped())
78            .spawn()?;
79        if let Some(stdio) = &mut rustfmt.stdin {
80            stdio.write_all(code.as_bytes()).await?;
81        }
82        let output = rustfmt.wait_with_output().await?;
83        String::from_utf8(output.stdout)?
84    }
85
86    /// Formats program code - nightly.
87    #[throws]
88    pub async fn format_program_code_nightly(code: &str) -> String {
89        let mut rustfmt = Command::new("rustfmt")
90            .arg("+nightly")
91            .arg("--config")
92            .arg(
93                "\
94            edition=2021,\
95            wrap_comments=true,\
96            normalize_doc_attributes=true",
97            )
98            .kill_on_drop(true)
99            .stdin(Stdio::piped())
100            .stdout(Stdio::piped())
101            .spawn()?;
102        if let Some(stdio) = &mut rustfmt.stdin {
103            stdio.write_all(code.as_bytes()).await?;
104        }
105        let output = rustfmt.wait_with_output().await?;
106        String::from_utf8(output.stdout)?
107    }
108
109    /// Manages a child process in an async context, specifically for monitoring fuzzing tasks.
110    /// Waits for the process to exit or a Ctrl+C signal. Prints an error message if the process
111    /// exits with an error, and sleeps briefly on Ctrl+C. Throws `Error::FuzzingFailed` on errors.
112    ///
113    /// # Arguments
114    /// * `child` - A mutable reference to a `Child` process.
115    ///
116    /// # Errors
117    /// * Throws `Error::FuzzingFailed` if waiting on the child process fails.
118    #[throws]
119    async fn handle_child(child: &mut Child, with_exit_code: bool) {
120        tokio::select! {
121            res = child.wait() =>
122                match res {
123                    Ok(status) => match status.code() {
124                        Some(code) => {
125                            match (code, with_exit_code) {
126                                (0, _) => {} // fuzzing did not find any failing invariants or panics and we dont care about exit code
127                                (99, true) => throw!(Error::FuzzingFailedInvariantOrPanic), // fuzzing found failing invariants or panics and we care about exit code
128                                (99, false) => {} // fuzzing found failing invariants or panics and we dont care about exit code
129                                (_, _) => throw!(Error::FuzzingFailed), // fuzzing failed for some other reason so we care about exit code
130                            }
131                        }
132                        None => throw!(Error::FuzzingFailed),
133                    },
134                    Err(e) => throw!(e),
135            },
136            _ = signal::ctrl_c() => {
137                let _res = child.wait().await?;
138
139                tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
140            },
141        }
142    }
143    #[throws]
144    pub async fn clean_target(&self) {
145        self.clean_anchor_target().await?;
146        self.clean_fuzz_target().await?;
147    }
148
149    #[throws]
150    async fn clean_anchor_target(&self) {
151        Command::new("anchor").arg("clean").spawn()?.wait().await?;
152    }
153
154    #[throws]
155    #[allow(dead_code)]
156    async fn clean_fuzz_target(&self) {
157        let trident_tests_dir = self.root.join(TESTS_WORKSPACE_DIRECTORY);
158        Command::new("cargo")
159            .arg("clean")
160            .current_dir(trident_tests_dir)
161            .spawn()?
162            .wait()
163            .await?;
164    }
165
166    pub fn get_target_dir(&self) -> Result<String, Error> {
167        let current_dir = env::current_dir()?;
168        let mut dir = Some(current_dir.as_path());
169        while let Some(cwd) = dir {
170            for file in std::fs::read_dir(cwd).with_context(|| {
171                format!("Error reading the directory with path: {}", cwd.display())
172            })? {
173                let path = file
174                    .with_context(|| {
175                        format!("Error reading the directory with path: {}", cwd.display())
176                    })?
177                    .path();
178                if let Some(filename) = path.file_name() {
179                    if filename.to_str() == Some(TESTS_WORKSPACE_DIRECTORY) {
180                        return Ok(path.join("target").to_str().unwrap().to_string());
181                    }
182                }
183            }
184            dir = cwd.parent();
185        }
186        throw!(Error::BadWorkspace);
187    }
188}