turdle_client/
commander.rs1use crate::{
2 idl::{self, Idl},
3 program_client_generator, Client,
4};
5use cargo_metadata::{MetadataCommand, Package};
6use fehler::{throw, throws};
7use futures::future::try_join_all;
8use log::debug;
9use solana_sdk::signer::keypair::Keypair;
10use std::{borrow::Cow, io, iter, path::Path, process::Stdio, string::FromUtf8Error};
11use thiserror::Error;
12use tokio::{
13 fs,
14 io::AsyncWriteExt,
15 process::{Child, Command},
16};
17
18pub static PROGRAM_CLIENT_DIRECTORY: &str = ".program_client";
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("localnet is not running")]
27 LocalnetIsNotRunning,
28 #[error("localnet is still running")]
29 LocalnetIsStillRunning,
30 #[error("build programs failed")]
31 BuildProgramsFailed,
32 #[error("testing failed")]
33 TestingFailed,
34 #[error("read program code failed: '{0}'")]
35 ReadProgramCodeFailed(String),
36 #[error("{0:?}")]
37 Idl(#[from] idl::Error),
38 #[error("{0:?}")]
39 TomlDeserialize(#[from] toml::de::Error),
40 #[error("parsing Cargo.toml dependencies failed")]
41 ParsingCargoTomlDependenciesFailed,
42}
43
44pub struct LocalnetHandle {
46 solana_test_validator_process: Child,
47}
48
49impl LocalnetHandle {
50 #[throws]
60 pub async fn stop(mut self) {
61 self.solana_test_validator_process.kill().await?;
62 if Client::new(Keypair::new()).is_localnet_running(false).await {
63 throw!(Error::LocalnetIsStillRunning);
64 }
65 debug!("localnet stopped");
66 }
67
68 #[throws]
79 pub async fn stop_and_remove_ledger(self) {
80 self.stop().await?;
81 fs::remove_dir_all("test-ledger").await?;
82 debug!("ledger removed");
83 }
84}
85
86pub struct Commander {
89 root: Cow<'static, str>,
90}
91
92impl Commander {
93 pub fn new() -> Self {
95 Self {
96 root: "../../".into(),
97 }
98 }
99
100 pub fn with_root(root: impl Into<Cow<'static, str>>) -> Self {
102 Self { root: root.into() }
103 }
104
105 #[throws]
107 pub async fn build_programs(&self) {
108 let success = Command::new("cargo")
109 .arg("build-bpf")
110 .arg("--")
111 .args(["-Z", "avoid-dev-deps"])
114 .spawn()?
115 .wait()
116 .await?
117 .success();
118 if !success {
119 throw!(Error::BuildProgramsFailed);
120 }
121 }
122
123 #[throws]
128 pub async fn run_tests(&self) {
129 let success = Command::new("cargo")
130 .arg("test")
131 .arg("--")
132 .arg("--nocapture")
133 .spawn()?
134 .wait()
135 .await?
136 .success();
137 if !success {
138 throw!(Error::TestingFailed);
139 }
140 }
141
142 #[throws]
146 pub async fn create_program_client_crate(&self) {
147 let crate_path = Path::new(self.root.as_ref()).join(PROGRAM_CLIENT_DIRECTORY);
148 if fs::metadata(&crate_path).await.is_ok() {
149 return;
150 }
151
152 fs::create_dir(&crate_path).await?;
156
157 let cargo_toml_content = include_str!(concat!(
158 env!("CARGO_MANIFEST_DIR"),
159 "/src/templates/program_client/Cargo.toml.tmpl"
160 ));
161 fs::write(crate_path.join("Cargo.toml"), &cargo_toml_content).await?;
162
163 let src_path = crate_path.join("src");
164 fs::create_dir(&src_path).await?;
165
166 let lib_rs_content = include_str!(concat!(
167 env!("CARGO_MANIFEST_DIR"),
168 "/src/templates/program_client/lib.rs"
169 ));
170 fs::write(src_path.join("lib.rs"), &lib_rs_content).await?;
171
172 debug!("program_client crate created")
173 }
174
175 pub fn program_packages(&self) -> impl Iterator<Item = Package> {
177 let cargo_toml_data = MetadataCommand::new()
178 .no_deps()
179 .exec()
180 .expect("Cargo.toml reading failed");
181
182 cargo_toml_data.packages.into_iter().filter(|package| {
183 if let Some("programs") = package.manifest_path.iter().nth_back(2) {
185 return true;
186 }
187 false
188 })
189 }
190
191 #[throws]
195 pub async fn generate_program_client_deps(&self) {
196 let trdelnik_dep = r#"turdle-client = "0.1.0""#.parse().unwrap();
197 let absolute_root = fs::canonicalize(self.root.as_ref()).await?;
211
212 let program_deps = self.program_packages().map(|package| {
213 let name = package.name;
214 let path = package
215 .manifest_path
216 .parent()
217 .unwrap()
218 .strip_prefix(&absolute_root)
219 .unwrap();
220 format!(r#"{name} = {{ path = "../{path}", features = ["no-entrypoint"] }}"#)
221 .parse()
222 .unwrap()
223 });
224
225 let cargo_toml_path = Path::new(self.root.as_ref())
226 .join(PROGRAM_CLIENT_DIRECTORY)
227 .join("Cargo.toml");
228
229 let mut cargo_toml_content: toml::Value =
230 fs::read_to_string(&cargo_toml_path).await?.parse()?;
231
232 let cargo_toml_deps = cargo_toml_content
233 .get_mut("dependencies")
234 .and_then(toml::Value::as_table_mut)
235 .ok_or(Error::ParsingCargoTomlDependenciesFailed)?;
236
237 for dep in iter::once(trdelnik_dep).chain(program_deps) {
238 if let toml::Value::Table(table) = dep {
239 let (name, value) = table.into_iter().next().unwrap();
240 cargo_toml_deps.entry(name).or_insert(value);
241 }
242 }
243
244 fs::write(cargo_toml_path, cargo_toml_content.to_string()).await?;
247 }
248
249 #[throws]
253 pub async fn generate_program_client_lib_rs(&self) {
254 let idl_programs = self.program_packages().map(|package| async move {
255 let name = package.name;
256 let output = Command::new("cargo")
257 .arg("+nightly")
258 .arg("rustc")
259 .args(["--package", &name])
260 .arg("--profile=check")
261 .arg("--")
262 .arg("-Zunpretty=expanded")
263 .output()
264 .await?;
265 if output.status.success() {
266 let code = String::from_utf8(output.stdout)?;
267 Ok(idl::parse_to_idl_program(name, &code).await?)
268 } else {
269 let error_text = String::from_utf8(output.stderr)?;
270 Err(Error::ReadProgramCodeFailed(error_text))
271 }
272 });
273 let idl = Idl {
274 programs: try_join_all(idl_programs).await?,
275 };
276 let use_tokens = self.parse_program_client_imports().await?;
277 let program_client = program_client_generator::generate_source_code(idl, &use_tokens);
278 let program_client = Self::format_program_code(&program_client).await?;
279
280 let rust_file_path = Path::new(self.root.as_ref())
281 .join(PROGRAM_CLIENT_DIRECTORY)
282 .join("src/lib.rs");
283 fs::write(rust_file_path, &program_client).await?;
284 }
285
286 #[throws]
288 pub async fn format_program_code(code: &str) -> String {
289 let mut rustfmt = Command::new("rustfmt")
290 .args(["--edition", "2018"])
291 .kill_on_drop(true)
292 .stdin(Stdio::piped())
293 .stdout(Stdio::piped())
294 .spawn()?;
295 if let Some(stdio) = &mut rustfmt.stdin {
296 stdio.write_all(code.as_bytes()).await?;
297 }
298 let output = rustfmt.wait_with_output().await?;
299 String::from_utf8(output.stdout)?
300 }
301
302 #[throws]
304 pub async fn start_localnet(&self) -> LocalnetHandle {
305 let mut process = Command::new("solana-test-validator")
306 .arg("-C")
307 .arg([&self.root, "config.yml"].concat())
308 .arg("-r")
309 .arg("-q")
310 .spawn()?;
311 if !Client::new(Keypair::new()).is_localnet_running(true).await {
312 process.kill().await.ok();
315 throw!(Error::LocalnetIsNotRunning);
316 }
317 debug!("localnet started");
318 LocalnetHandle {
319 solana_test_validator_process: process,
320 }
321 }
322
323 #[throws]
328 pub async fn parse_program_client_imports(&self) -> Vec<syn::ItemUse> {
329 let output = Command::new("cargo")
330 .arg("+nightly")
331 .arg("rustc")
332 .args(["--package", "program_client"])
333 .arg("--profile=check")
334 .arg("--")
335 .arg("-Zunpretty=expanded")
336 .output()
337 .await?;
338 let code = String::from_utf8(output.stdout)?;
339 let mut use_modules: Vec<syn::ItemUse> = vec![];
340 for item in syn::parse_file(code.as_str()).unwrap().items.into_iter() {
341 if let syn::Item::Mod(module) = item {
342 let modules = module
343 .content
344 .ok_or("account mod: empty content")
345 .unwrap()
346 .1
347 .into_iter();
348 for module in modules {
349 if let syn::Item::Use(u) = module {
350 use_modules.push(u);
351 }
352 }
353 }
354 }
355 if use_modules.is_empty() {
356 use_modules.push(syn::parse_quote! { use trdelnik_client::*; })
357 }
358 use_modules
359 }
360}
361
362impl Default for Commander {
363 fn default() -> Self {
365 Self::new()
366 }
367}