Skip to content

Commit 2221f05

Browse files
committed
Added tests
1 parent 3f50fff commit 2221f05

File tree

3 files changed

+163
-12
lines changed

3 files changed

+163
-12
lines changed

crates/turborepo-ui/src/wui/sender.rs

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ pub struct WebUISender {
1414
}
1515

1616
impl WebUISender {
17+
pub fn new(tx: tokio::sync::mpsc::UnboundedSender<WebUIEvent>) -> Self {
18+
Self { tx }
19+
}
1720
pub fn start_task(&self, task: String, output_logs: OutputLogs) {
1821
self.tx
1922
.send(WebUIEvent::StartTask { task, output_logs })

crates/turborepo-ui/src/wui/server.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,16 @@ impl<'a> CurrentRun<'a> {
4242
}
4343
}
4444

45-
struct Query {
45+
pub struct Query {
4646
state: Arc<Mutex<RefCell<WebUIState>>>,
4747
}
4848

49+
impl Query {
50+
pub fn new(state: Arc<Mutex<RefCell<WebUIState>>>) -> Self {
51+
Self { state }
52+
}
53+
}
54+
4955
#[Object]
5056
impl Query {
5157
async fn current_run(&self) -> CurrentRun {
@@ -67,8 +73,14 @@ pub async fn start_server(
6773
) -> Result<(), crate::Error> {
6874
let state = Arc::new(Mutex::new(RefCell::new(WebUIState::default())));
6975
let subscriber = Subscriber::new(rx);
70-
subscriber.watch(state.clone());
76+
tokio::spawn(subscriber.watch(state.clone()));
77+
78+
run_server(state.clone()).await?;
79+
80+
Ok(())
81+
}
7182

83+
pub(crate) async fn run_server(state: Arc<Mutex<RefCell<WebUIState>>>) -> Result<(), crate::Error> {
7284
let cors = CorsLayer::new()
7385
// allow `GET` and `POST` when accessing the resource
7486
.allow_methods([Method::GET, Method::POST])

crates/turborepo-ui/src/wui/subscriber.rs

+146-10
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,15 @@ impl Subscriber {
1919
Self { rx }
2020
}
2121

22-
pub fn watch(
22+
pub async fn watch(
2323
self,
2424
// We use a tokio::sync::Mutex here because we want this future to be Send.
2525
#[allow(clippy::type_complexity)] state: Arc<Mutex<RefCell<WebUIState>>>,
2626
) {
27-
tokio::spawn(async move {
28-
let mut rx = self.rx;
29-
while let Some(event) = rx.recv().await {
30-
Self::add_message(&state, event).await;
31-
}
32-
});
27+
let mut rx = self.rx;
28+
while let Some(event) = rx.recv().await {
29+
Self::add_message(&state, event).await;
30+
}
3331
}
3432

3533
async fn add_message(state: &Arc<Mutex<RefCell<WebUIState>>>, event: WebUIEvent) {
@@ -69,8 +67,10 @@ impl Subscriber {
6967
} => {
7068
let mut state_ref = state.borrow_mut();
7169

70+
if result == CacheResult::Hit {
71+
state_ref.tasks.get_mut(&task).unwrap().status = TaskStatus::Cached;
72+
}
7273
state_ref.tasks.get_mut(&task).unwrap().cache_result = Some(result);
73-
7474
state_ref.tasks.get_mut(&task).unwrap().cache_message = Some(message);
7575
}
7676
WebUIEvent::Stop => {
@@ -118,13 +118,13 @@ pub enum TaskStatus {
118118
Running,
119119
Cached,
120120
Failed,
121-
Success,
121+
Succeeded,
122122
}
123123

124124
impl From<TaskResult> for TaskStatus {
125125
fn from(result: TaskResult) -> Self {
126126
match result {
127-
TaskResult::Success => Self::Success,
127+
TaskResult::Success => Self::Succeeded,
128128
TaskResult::CacheHit => Self::Cached,
129129
TaskResult::Failure => Self::Failed,
130130
}
@@ -150,3 +150,139 @@ impl WebUIState {
150150
&self.tasks
151151
}
152152
}
153+
154+
#[cfg(test)]
155+
mod test {
156+
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
157+
158+
use super::*;
159+
use crate::{
160+
tui::event::OutputLogs,
161+
wui::{sender::WebUISender, server::Query},
162+
};
163+
164+
#[tokio::test]
165+
async fn test_web_ui_state() -> Result<(), crate::Error> {
166+
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
167+
let state = Arc::new(Mutex::new(RefCell::new(WebUIState::default())));
168+
let subscriber = Subscriber::new(rx);
169+
170+
let sender = WebUISender::new(tx);
171+
172+
// Start a successful task
173+
sender.start_task("task".to_string(), OutputLogs::Full);
174+
sender.output("task".to_string(), b"this is my output".to_vec())?;
175+
sender.end_task("task".to_string(), TaskResult::Success);
176+
177+
// Start a cached task
178+
sender.start_task("task2".to_string(), OutputLogs::Full);
179+
sender.status("task2".to_string(), "status".to_string(), CacheResult::Hit);
180+
181+
// Start a failing task
182+
sender.start_task("task3".to_string(), OutputLogs::Full);
183+
sender.end_task("task3".to_string(), TaskResult::Failure);
184+
185+
// Drop the sender so the subscriber can terminate
186+
drop(sender);
187+
188+
// Run the subscriber blocking
189+
subscriber.watch(state.clone()).await;
190+
191+
let state_handle = state.lock().await.borrow().clone();
192+
assert_eq!(state_handle.tasks().len(), 3);
193+
assert_eq!(
194+
state_handle.tasks().get("task2").unwrap().status,
195+
TaskStatus::Cached
196+
);
197+
assert_eq!(
198+
state_handle.tasks().get("task").unwrap().status,
199+
TaskStatus::Succeeded
200+
);
201+
assert_eq!(
202+
state_handle.tasks().get("task").unwrap().output,
203+
b"this is my output"
204+
);
205+
assert_eq!(
206+
state_handle.tasks().get("task3").unwrap().status,
207+
TaskStatus::Failed
208+
);
209+
210+
// Now let's check with the GraphQL API
211+
let schema = Schema::new(Query::new(state), EmptyMutation, EmptySubscription);
212+
let result = schema
213+
.execute("query { currentRun { tasks { name state { status } } } }")
214+
.await;
215+
assert!(result.errors.is_empty());
216+
assert_eq!(
217+
result.data,
218+
async_graphql::Value::from_json(serde_json::json!({
219+
"currentRun": {
220+
"tasks": [
221+
{
222+
"name": "task",
223+
"state": {
224+
"status": "SUCCEEDED"
225+
}
226+
},
227+
{
228+
"name": "task2",
229+
"state": {
230+
"status": "CACHED"
231+
}
232+
},
233+
{
234+
"name": "task3",
235+
"state": {
236+
"status": "FAILED"
237+
}
238+
}
239+
]
240+
}
241+
}))
242+
.unwrap()
243+
);
244+
245+
Ok(())
246+
}
247+
248+
#[tokio::test]
249+
async fn test_restart_tasks() -> Result<(), crate::Error> {
250+
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
251+
let state = Arc::new(Mutex::new(RefCell::new(WebUIState::default())));
252+
let subscriber = Subscriber::new(rx);
253+
254+
let sender = WebUISender::new(tx);
255+
256+
// Start a successful task
257+
sender.start_task("task".to_string(), OutputLogs::Full);
258+
sender.output("task".to_string(), b"this is my output".to_vec())?;
259+
sender.end_task("task".to_string(), TaskResult::Success);
260+
261+
// Start a cached task
262+
sender.start_task("task2".to_string(), OutputLogs::Full);
263+
sender.status("task2".to_string(), "status".to_string(), CacheResult::Hit);
264+
265+
// Restart a task
266+
sender.restart_tasks(vec!["task".to_string()])?;
267+
268+
// Drop the sender so the subscriber can terminate
269+
drop(sender);
270+
271+
// Run the subscriber blocking
272+
subscriber.watch(state.clone()).await;
273+
274+
let state_handle = state.lock().await.borrow().clone();
275+
assert_eq!(state_handle.tasks().len(), 3);
276+
assert_eq!(
277+
state_handle.tasks().get("task2").unwrap().status,
278+
TaskStatus::Cached
279+
);
280+
assert_eq!(
281+
state_handle.tasks().get("task").unwrap().status,
282+
TaskStatus::Running
283+
);
284+
assert_eq!(state_handle.tasks().get("task").unwrap().output, vec![]);
285+
286+
Ok(())
287+
}
288+
}

0 commit comments

Comments
 (0)