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