omry_integration/
helpers.rs1use anyhow::{Context, Result};
3use fluent_uri::Uri;
4use omry_archiving::{Document, Marshal, Record, RecordParams, ToRecord};
5use omry_service::iroh::EndpointAddr;
6use std::env;
7use std::fs::{self, File};
8use std::io::prelude::*;
9use std::net::{Ipv4Addr, Ipv6Addr, SocketAddrV4, SocketAddrV6, TcpListener, ToSocketAddrs};
10use std::path::{Path, PathBuf};
11use tempfile::{TempDir, tempdir};
12use testcontainers::core::Mount;
13use tracing::Level;
14use tracing_subscriber::FmtSubscriber;
15use url::Url;
16use uuid::Uuid;
17
18use crate::db::TempDb;
19
20const DOCKER_HOST_KEY: &str = "DOCKER_HOST";
22
23const WORKSPACE_DIR_KEY: &str = "CARGO_WORKSPACE_DIR";
25
26const TEST_ARCHIVE_SUBPATH: &str = "test_data/example.postcard";
28
29const TEST_LONG_DOC_SUBPATH: &str = "test_data/lipsum.html";
32
33const FIND_PORT_MAX_RETRIES: u8 = 5;
35
36fn free_port_from_socket_addr(
42 socket: impl ToSocketAddrs,
43 max_attempts: u8,
44) -> anyhow::Result<(TcpListener, u16)> {
45 let mut attempts_left = max_attempts;
46 let socket_addrs = socket.to_socket_addrs()?.collect::<Vec<_>>();
47
48 while attempts_left > 0 {
49 if let Ok(listener) = TcpListener::bind(socket_addrs.as_slice()) {
50 let port = listener.local_addr()?.port();
51 return Ok((listener, port));
52 }
53 attempts_left -= 1;
54 }
55
56 anyhow::bail!("Couldn't bind a listener to a port in {max_attempts} attempts.");
57}
58
59#[must_use]
68pub fn free_port_ipv4() -> (TcpListener, u16) {
69 let socket = SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0);
70
71 #[allow(clippy::expect_used)]
74 free_port_from_socket_addr(socket, FIND_PORT_MAX_RETRIES).expect("Couldn't find a free port.")
75}
76
77#[must_use]
82pub fn free_port_ipv6() -> (TcpListener, u16) {
83 let socket = SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0);
84
85 #[allow(clippy::expect_used)]
88 free_port_from_socket_addr(socket, FIND_PORT_MAX_RETRIES).expect("Couldn't find a free port.")
89}
90
91#[derive(Debug, Clone)]
93pub(crate) struct DockerHost {
94 uri: Uri<String>,
95}
96
97impl DockerHost {
98 pub(crate) fn from_env() -> Result<Self> {
107 let uri = env::var(DOCKER_HOST_KEY)
108 .map_err(|e| {
109 anyhow::anyhow!("Failed to get the docker host from the environment: {e}")
110 })?
111 .parse()
112 .map_err(|e| anyhow::anyhow!("Can't parse the docker host: {e}"))?;
113
114 Ok(Self { uri })
115 }
116
117 pub(crate) fn is_remote(&self) -> bool {
125 self.uri.scheme().as_str() == "tcp" && self.host().is_some()
126 }
127
128 pub(crate) fn host(&self) -> Option<&str> {
132 Some(self.uri.authority()?.host())
133 }
134}
135
136pub fn path_in_project(subpath: &str) -> Result<PathBuf> {
145 let (_, workspace_dir_path) = env::vars()
146 .find(|(k, _)| k == WORKSPACE_DIR_KEY)
147 .ok_or_else(|| anyhow::anyhow!("{WORKSPACE_DIR_KEY} is not set"))?;
148
149 let workspace_dir_path = fs::canonicalize(workspace_dir_path)?;
150 Ok(workspace_dir_path.join(subpath))
151}
152
153pub fn make_test_archive_holder() -> anyhow::Result<Document> {
163 let test_archive_path = path_in_project(TEST_ARCHIVE_SUBPATH)?;
164 let mut file = File::open(test_archive_path)?;
165 let mut buf = vec![];
166 file.read_to_end(&mut buf)?;
167 let archive_holder = Document::from_bytes(&buf)?;
168 Ok(archive_holder)
169}
170
171pub fn make_test_record_params() -> anyhow::Result<RecordParams> {
176 let document = make_test_archive_holder()?;
177 let url = "https://example.org/".to_string();
178 let title = "Example".to_string();
179 let client_datetime = "2024-10-11T13:49:46-05:00".to_string();
180 let language = Some("en".to_string());
181
182 Ok(RecordParams {
183 id: None,
184 url,
185 title,
186 language,
187 client_datetime,
188 document,
189 timestamp_flora: None,
190 updated_at: None,
191 })
192}
193
194pub fn make_test_record() -> anyhow::Result<Record> {
199 let incoming_record = Record::new(make_test_record_params()?);
200 Ok(incoming_record)
201}
202
203fn load_long_doc() -> anyhow::Result<Document> {
204 let (_, workspace_dir_path) = env::vars()
205 .find(|(k, _)| k == WORKSPACE_DIR_KEY)
206 .ok_or_else(|| anyhow::anyhow!("{WORKSPACE_DIR_KEY} is not set"))?;
207 let workspace_dir_path = fs::canonicalize(workspace_dir_path)?;
208 let test_long_doc_path = workspace_dir_path.join(TEST_LONG_DOC_SUBPATH);
209
210 let mut file = File::open(&test_long_doc_path)?;
211 let mut buf = vec![];
212 file.read_to_end(&mut buf)?;
213
214 let archive_holder = Document::new_web_page_unchecked(buf);
215 Ok(archive_holder)
216}
217
218pub fn make_long_doc_record_params() -> anyhow::Result<RecordParams> {
223 let document = load_long_doc()?;
224 let url = "https://www.lipsum.com".to_string();
225 let title = "Lorem Ipsum";
226 let client_datetime = "2026-09-02T12:00:00-05:00";
227
228 #[allow(clippy::unreadable_literal)]
229 let timestamp_flora = Some(1788371331);
230 let language = Some("en".to_string());
231
232 Ok(RecordParams {
233 id: None,
234 url,
235 title: title.to_string(),
236 client_datetime: client_datetime.to_string(),
237 timestamp_flora,
238 language,
239 document,
240 updated_at: None,
241 })
242}
243
244pub async fn make_test_db_records(how_many: usize) -> anyhow::Result<Vec<impl ToRecord>> {
254 let db = TempDb::new().await?;
255 let mut db_records = Vec::with_capacity(how_many);
256 for _ in 0..how_many {
257 let record = make_test_record()?;
258 let row_id = db.insert(record.try_into()?).await?;
259 let db_record = db.get_record(row_id).await?;
260 db_records.push(db_record);
261 }
262 Ok(db_records)
263}
264
265#[derive(Debug)]
267pub struct TempPathWithString {
268 path: PathBuf,
270
271 path_string: String,
274}
275
276impl TempPathWithString {
277 pub fn new() -> anyhow::Result<Self> {
284 let temp_path = tempdir()?.keep();
285
286 #[allow(clippy::unnecessary_debug_formatting)]
290 let temp_path_string = temp_path
291 .to_str()
292 .ok_or_else(|| anyhow::anyhow!("Couldn't get a path string from {temp_path:?}"))?
293 .to_string();
294
295 Ok(Self {
296 path: temp_path,
297 path_string: temp_path_string,
298 })
299 }
300
301 #[must_use]
303 pub fn as_path(&self) -> &Path {
304 &self.path
305 }
306
307 #[must_use]
309 pub fn as_str(&self) -> &str {
310 &self.path_string
311 }
312}
313
314pub fn init_tracing_subscriber() -> anyhow::Result<()> {
320 let subscriber = FmtSubscriber::builder()
321 .with_max_level(Level::INFO)
322 .finish();
323 tracing::subscriber::set_global_default(subscriber)?;
324
325 Ok(())
326}
327
328#[derive(Debug)]
333pub struct TempTracingLocation {
334 dir: TempDir,
336
337 dir_path: String,
342
343 file_prefix: String,
345}
346
347impl TempTracingLocation {
348 pub fn new() -> Result<Self> {
356 let dir = TempDir::new()?;
357 let dir_path = dir
358 .path()
359 .to_str()
360 .ok_or_else(|| anyhow::anyhow!("Cannot get the string path to the tracing dir."))?
361 .to_string();
362
363 let file_prefix = Uuid::now_v7().to_string();
364
365 Ok(Self {
366 dir,
367 dir_path,
368 file_prefix,
369 })
370 }
371
372 #[must_use]
374 pub fn dir(&self) -> &TempDir {
375 &self.dir
376 }
377
378 #[must_use]
380 pub fn path(&self) -> &str {
381 &self.dir_path
382 }
383
384 #[must_use]
386 pub fn file_prefix(&self) -> &str {
387 &self.file_prefix
388 }
389
390 pub fn read_first_trace(&self) -> Result<Option<String>> {
401 let dir = fs::read_dir(self.dir())?;
402
403 for entry in dir {
404 let entry = entry?;
405 let file_name = entry.file_name();
406 let file_name = file_name.to_string_lossy();
407
408 if file_name.starts_with(self.file_prefix()) {
409 let output = fs::read_to_string(entry.path())?;
410 return Ok(Some(output));
411 }
412 }
413
414 Ok(None)
415 }
416}
417
418#[derive(Debug, Copy, Clone)]
420pub struct HostPorts {
421 pub rpc: u16,
423
424 pub web: u16,
426}
427
428impl HostPorts {
429 #[must_use]
435 pub(crate) fn auto() -> (Self, (TcpListener, TcpListener)) {
436 let (rpc_listener, rpc) = free_port_ipv6();
437 let (web_listener, web) = free_port_ipv6();
438 (Self { rpc, web }, (rpc_listener, web_listener))
439 }
440}
441
442#[derive(Debug)]
444pub struct ContainerMount {
445 pub mount: Mount,
447
448 pub temp_path: Option<TempPathWithString>,
450}
451
452impl ContainerMount {
453 pub fn new(container_path: &str) -> Result<Self> {
466 if let Ok(docker_host) = DockerHost::from_env()
467 && docker_host.is_remote()
468 {
469 let mount = Mount::tmpfs_mount(container_path);
470 return Ok(Self {
471 mount,
472 temp_path: None,
473 });
474 }
475
476 let temp_path = TempPathWithString::new()?;
477 let mount = Mount::bind_mount(temp_path.as_str(), container_path);
478
479 Ok(Self {
480 mount,
481 temp_path: Some(temp_path),
482 })
483 }
484}
485
486#[allow(clippy::missing_errors_doc)]
489pub async fn endpoint_addr(server_root: &str) -> Result<EndpointAddr> {
490 const ENDPOINT_SUBPATH: &str = "/endpoint-addr";
491 let url = Url::parse(server_root).context("error parsing the URL")?;
492 let url = url.join(ENDPOINT_SUBPATH)?;
493 let bytes = reqwest::get(url)
494 .await?
495 .error_for_status()
496 .context("Couldn't get the endpoint_address")?
497 .bytes()
498 .await
499 .context("Couldn't read the bytes")?;
500 let endpoint_addr =
501 postcard::from_bytes(&bytes).context("Error deserializing the endpoint address")?;
502 Ok(endpoint_addr)
503}
504
505pub mod token {
510 use rand::RngExt;
511 use rand::distr::Alphanumeric;
512 use rand::rngs::ChaCha20Rng;
513
514 pub fn generate(len: usize) -> String {
516 let rng = rand::make_rng::<ChaCha20Rng>();
517 rng.sample_iter(Alphanumeric)
518 .take(len)
519 .map(char::from)
520 .collect()
521 }
522}