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