1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
//! "Stack" that includes a simple Omry client app, to facilitate integration tests
//! with a containerised Omry server.
use crate::flora_stack::{FloraStack, FloraStackOptions};
use anyhow::Result;
use std::sync::Arc;
use std::time::Duration;
pub use test_app::App;
use tracing::instrument;
/// Omry client app for tests.
mod test_app {
use crate::bridge::ServiceBridge;
use crate::flora_stack::{self, FloraStack};
use anyhow::{Context, Result};
use omry_service::FloraServiceClient;
use omry_service::search::{SearchParameters, SearchResponse, SortBy};
use omry_service::tarpc::context;
use std::sync::Arc;
use tokio::time::{self, Duration};
use tracing::instrument;
/// Client "app" for testing.
#[derive(Debug, Clone)]
pub struct App {
/// Bridge API for the flora tarpc backend.
client: FloraServiceClient,
}
impl App {
/// Initializes the app state.
#[must_use]
pub fn new(client: FloraServiceClient) -> Self {
Self { client }
}
/// Constructs [`SearchParameters`] with the given text query,
/// and sends a search request to the Omry backend.
///
/// Adds `sort_by` to the search parameters to sort hits by recency.
///
/// ## Errors
/// Returns [`anyhow::Error`] if the search RPC fails.
#[instrument]
pub async fn search(&self, query: String, page: Option<usize>) -> Result<SearchResponse> {
const BUCKETS: u32 = 5;
const QUERY_BY: &[&str] = &["title", "text-body"];
let query_by = QUERY_BY
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>();
let sort_by = SortBy::relevance_and_recency(BUCKETS);
let page = page.unwrap_or(1);
let params = SearchParameters::new_query(query, query_by)
.num_typos(2)
.sort_by(sort_by)
.page(page);
ServiceBridge::search(self, params.into())
.await
.context("Couldn't call search")
}
}
impl ServiceBridge for App {
fn client(&self) -> &FloraServiceClient {
&self.client
}
}
/// Initializes a test client app, and checks that it can communicate
/// with the Omry server.
#[instrument]
pub(super) async fn init_app(flora_stack: Arc<FloraStack>) -> Result<App> {
const MAX_CONN_ATTEMPTS: usize = 5;
const BACKOFF_MULTIPLIER: u32 = 2;
tokio::spawn(async move {
let mut attempts_so_far = 0;
let mut backoff_interval = Duration::from_secs(1);
// Attempt to first create an Omry RPC client, then use that client
// to initialize our RPC bridge.
//
// We need to recreate the client on every iteration of the loop
// even if we initialized it successfully in a previous iteration
// because the client's connection to the tarpc server can shut down
// when we use tokio::time::sleep.
loop {
match flora_stack::flora_client(flora_stack.flora_container()).await {
Ok(client) => match client.ping(context::current()).await {
Ok(_) => break Ok(App::new(client)),
Err(error) => {
tracing::warn!("error while initializing the tarpc client: {error}");
}
},
Err(error) => {
tracing::warn!("error while creating the flora client: {error}");
}
}
if attempts_so_far == MAX_CONN_ATTEMPTS {
anyhow::bail!("couldn't succeed after {MAX_CONN_ATTEMPTS} attempts");
}
time::sleep(backoff_interval).await;
backoff_interval *= BACKOFF_MULTIPLIER;
attempts_so_far += 1;
}
})
.await?
}
}
/// Flora test stack.
#[derive(Debug)]
pub struct OmryFullStack {
/// Containerised Omry stack.
backend: Arc<FloraStack>,
/// Simple 'client' app.
app: App,
}
impl OmryFullStack {
/// Creates a new test stack that includes an Omry backend (`omry-server` and Typesense),
/// and a client app to facilitate integration testing.
///
/// ## Errors
///
/// Returns [`anyhow::Error`] if we fail to:
/// - Initialize the backend stack
/// - Initialize the client app and connect it to that backend stack
#[instrument]
pub async fn new() -> Result<Self> {
let stack_options = FloraStackOptions {
search_sync_interval: Duration::from_secs(5),
..Default::default()
};
let backend = Arc::new(FloraStack::new(Some(stack_options)).await?);
let app = test_app::init_app(Arc::clone(&backend)).await?;
tracing::info!("successfully initialized the test bridge");
Ok(Self { backend, app })
}
/// Returns a reference to the test client app.
#[must_use]
pub fn app(&self) -> &App {
&self.app
}
/// Attempts to stop ("kill") this Omry stack, shutting down
/// the connection(s) with the Omry backend.
///
/// ## Errors
/// Returns [`anyhow::Error`] if we can't stop the Omry backend.
pub async fn kill_flora_rpc(&mut self) -> Result<()> {
self.backend.stop().await?;
Ok(())
}
}