Skip to main content

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!("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                    // Hack: let's add a pause after mutations.
131                    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
143/// Returns an HTML response constructing the GraphiQL web page for the given URI.
144pub(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
161/// Converts a `Duration` to `Option<Duration>`, treating zero as `None`.
162pub 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/// Checks the condition five times with increasing delays. Returns `true` if it is met.
183#[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}