linera_service/
util.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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
21// Exported for readme e2e tests.
22pub static DEFAULT_PAUSE_AFTER_LINERA_SERVICE_SECS: &str = "3";
23pub static DEFAULT_PAUSE_AFTER_GQL_MUTATIONS_SECS: &str = "3";
24
25/// Extension trait for [`tokio::process::Child`].
26pub 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                    // Hack: let's add a pause after mutations.
135                    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
147/// Returns an HTML response constructing the GraphiQL web page for the given URI.
148pub(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/// Checks the condition five times with increasing delays. Returns true if it is met.
170#[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}