1use std::{
5 io::{BufRead, BufReader, Write},
6 num::ParseIntError,
7 path::Path,
8 time::Duration,
9};
10
11use anyhow::{bail, Context as _, Result};
12use async_graphql::http::GraphiQLSource;
13use axum::response::{self, IntoResponse};
14use http::Uri;
15#[cfg(test)]
16use linera_base::command::parse_version_message;
17use linera_base::data_types::TimeDelta;
18pub use linera_client::util::*;
19use tracing::debug;
20
21pub static DEFAULT_PAUSE_AFTER_LINERA_SERVICE_SECS: &str = "3";
23pub static DEFAULT_PAUSE_AFTER_GQL_MUTATIONS_SECS: &str = "3";
25
26pub trait ChildExt: std::fmt::Debug {
28 fn ensure_is_running(&mut self) -> Result<()>;
30}
31
32impl ChildExt for tokio::process::Child {
33 fn ensure_is_running(&mut self) -> Result<()> {
34 if let Some(status) = self.try_wait().context("try_wait child process")? {
35 bail!("Child process {self:?} already exited with status: {status}");
36 }
37 debug!("Child process {self:?} is running as expected.");
38 Ok(())
39 }
40}
41
42pub fn read_json<T: serde::de::DeserializeOwned>(path: impl Into<std::path::PathBuf>) -> Result<T> {
44 Ok(serde_json::from_reader(fs_err::File::open(path)?)?)
45}
46
47#[cfg(with_testing)]
49#[macro_export]
50macro_rules! test_name {
51 () => {
52 stdext::function_name!()
53 .strip_suffix("::{{closure}}")
54 .expect("should be called from the body of a test")
55 };
56}
57
58pub struct Markdown<B> {
60 buffer: B,
61}
62
63impl Markdown<BufReader<fs_err::File>> {
64 pub fn new(path: impl AsRef<Path>) -> std::io::Result<Self> {
66 let buffer = BufReader::new(fs_err::File::open(path.as_ref())?);
67 Ok(Self { buffer })
68 }
69}
70
71impl<B> Markdown<B>
72where
73 B: BufRead,
74{
75 #[expect(clippy::while_let_on_iterator)]
77 pub fn extract_bash_script_to(
78 self,
79 mut output: impl Write,
80 pause_after_linera_service: Option<Duration>,
81 pause_after_gql_mutations: Option<Duration>,
82 ) -> std::io::Result<()> {
83 let mut lines = self.buffer.lines();
84
85 while let Some(line) = lines.next() {
86 let line = line?;
87
88 if line.starts_with("```bash") {
89 if line.ends_with("ignore") {
90 continue;
91 } else {
92 let mut quote = String::new();
93 while let Some(line) = lines.next() {
94 let line = line?;
95 if line.starts_with("```") {
96 break;
97 }
98 quote += &line;
99 quote += "\n";
100
101 if let Some(pause) = pause_after_linera_service {
102 if line.contains("linera service") {
103 quote += &format!("sleep {}\n", pause.as_secs());
104 }
105 }
106 }
107 writeln!(output, "{quote}")?;
108 }
109 } else if let Some(uri) = line.strip_prefix("```gql,uri=") {
110 let mut quote = String::new();
111 while let Some(line) = lines.next() {
112 let line = line?;
113 if line.starts_with("```") {
114 break;
115 }
116 quote += &line;
117 quote += "\n";
118 }
119
120 writeln!(output, "QUERY=\"{}\"", quote.replace('"', "\\\""))?;
121 writeln!(
122 output,
123 "JSON_QUERY=$( jq -n --arg q \"$QUERY\" '{{\"query\": $q}}' )"
124 )?;
125 writeln!(
126 output,
127 "QUERY_RESULT=$( \
128 curl -w '\\n' -g -X POST \
129 -H \"Content-Type: application/json\" \
130 -d \"$JSON_QUERY\" {uri} \
131 | tee /dev/stderr \
132 | jq -e .data \
133 )"
134 )?;
135
136 if let Some(pause) = pause_after_gql_mutations {
137 if quote.starts_with("mutation") {
139 writeln!(output, "sleep {}\n", pause.as_secs())?;
140 }
141 }
142 }
143 }
144
145 output.flush()?;
146 Ok(())
147 }
148}
149
150pub(crate) async fn graphiql(uri: Uri) -> impl IntoResponse {
152 let source = GraphiQLSource::build()
153 .endpoint(uri.path())
154 .subscription_endpoint("/ws")
155 .finish()
156 .replace("@17", "@18")
157 .replace(
158 "ReactDOM.render(",
159 "ReactDOM.createRoot(document.getElementById(\"graphiql\")).render(",
160 );
161 response::Html(source)
162}
163
164pub fn parse_millis(s: &str) -> Result<Duration, ParseIntError> {
166 Ok(Duration::from_millis(s.parse()?))
167}
168
169pub fn non_zero_duration(d: Duration) -> Option<Duration> {
171 if d.is_zero() {
172 None
173 } else {
174 Some(d)
175 }
176}
177
178pub fn parse_millis_delta(s: &str) -> Result<TimeDelta, ParseIntError> {
180 Ok(TimeDelta::from_millis(s.parse()?))
181}
182
183pub fn parse_ascii_alphanumeric_string(s: &str) -> Result<String, &'static str> {
185 if s.chars().all(|x| x.is_ascii_alphanumeric()) {
186 Ok(s.to_string())
187 } else {
188 Err("Expecting ASCII alphanumeric characters")
189 }
190}
191
192#[cfg(with_testing)]
194pub async fn eventually<F>(condition: impl Fn() -> F) -> bool
195where
196 F: std::future::Future<Output = bool>,
197{
198 for i in 0..5 {
199 linera_base::time::timer::sleep(linera_base::time::Duration::from_secs(i)).await;
200 if condition().await {
201 return true;
202 }
203 }
204 false
205}
206
207#[test]
208fn test_parse_version_message() {
209 let s = "something\n . . . version12\nother things";
210 assert_eq!(parse_version_message(s), "version12");
211
212 let s = "something\n . . . version12other things";
213 assert_eq!(parse_version_message(s), "things");
214
215 let s = "something . . . version12 other things";
216 assert_eq!(parse_version_message(s), "");
217
218 let s = "";
219 assert_eq!(parse_version_message(s), "");
220}
221
222#[test]
223fn test_ignore() {
224 let readme = r#"
225first line
226```bash
227some bash
228```
229second line
230```bash
231some other bash
232```
233third line
234```bash,ignore
235this will be ignored
236```
237 "#;
238 let buffer = std::io::Cursor::new(readme);
239 let markdown = Markdown { buffer };
240 let mut script = Vec::new();
241 markdown
242 .extract_bash_script_to(&mut script, None, None)
243 .unwrap();
244 let expected = "some bash\n\nsome other bash\n\n";
245 assert_eq!(String::from_utf8_lossy(&script), expected);
246}