turbopack_bench/util/
prepared_app.rs1use std::{
2 future::Future,
3 path::{Path, PathBuf},
4 process::Child,
5 sync::Arc,
6};
7
8use anyhow::{Context, Result, anyhow};
9use chromiumoxide::{
10 Browser, Page,
11 cdp::{
12 browser_protocol::network::EventResponseReceived,
13 js_protocol::runtime::{AddBindingParams, EventBindingCalled, EventExceptionThrown},
14 },
15};
16use futures::{FutureExt, StreamExt};
17use tokio::{sync::Semaphore, task::spawn_blocking};
18use url::Url;
19
20use crate::{BINDING_NAME, bundlers::Bundler, util::PageGuard};
21
22async fn copy_dir(from: PathBuf, to: PathBuf) -> anyhow::Result<()> {
23 copy_dir_inner(from, to, Arc::new(Semaphore::new(64))).await
24}
25
26fn copy_dir_inner_send(
29 from: PathBuf,
30 to: PathBuf,
31 semaphore: Arc<Semaphore>,
32) -> impl Future<Output = anyhow::Result<()>> + Send {
33 copy_dir_inner(from, to, semaphore)
34}
35
36async fn copy_dir_inner(
37 from: PathBuf,
38 to: PathBuf,
39 semaphore: Arc<Semaphore>,
40) -> anyhow::Result<()> {
41 let mut jobs = Vec::new();
42 {
43 let _permit = semaphore
44 .acquire()
45 .await
46 .expect("semaphore is never closed");
47 let mut dir = spawn_blocking(|| std::fs::read_dir(from)).await??;
48 for entry in &mut dir {
49 let entry = entry?;
50 let ty = entry.file_type()?;
51 let to = to.join(entry.file_name());
52 if ty.is_dir() {
53 let semaphore = semaphore.clone();
54 jobs.push(tokio::spawn(async move {
55 tokio::fs::create_dir(&to).await?;
56 copy_dir_inner_send(entry.path(), to, semaphore).await
57 }));
58 } else if ty.is_file() {
59 let semaphore = semaphore.clone();
60 jobs.push(tokio::spawn(async move {
61 let _permit = semaphore
62 .acquire()
63 .await
64 .expect("semaphore is never closed");
65 tokio::fs::copy(entry.path(), to).await?;
66 Ok::<_, anyhow::Error>(())
67 }));
68 }
69 }
70 }
71
72 for job in jobs {
73 job.await??;
74 }
75
76 Ok(())
77}
78
79enum PreparedDir {
80 TempDir(tempfile::TempDir),
81 Path(PathBuf),
82}
83
84pub struct PreparedApp<'a> {
85 bundler: &'a dyn Bundler,
86 server: Option<(Child, String)>,
87 test_dir: PreparedDir,
88}
89
90impl<'a> PreparedApp<'a> {
91 pub async fn new(bundler: &'a dyn Bundler, template_dir: PathBuf) -> Result<PreparedApp<'a>> {
92 let test_dir = tempfile::tempdir()?;
93
94 tokio::fs::create_dir_all(&test_dir).await?;
95 copy_dir(template_dir, test_dir.path().to_path_buf()).await?;
96
97 Ok(Self {
98 bundler,
99 server: None,
100 test_dir: PreparedDir::TempDir(test_dir),
101 })
102 }
103
104 pub async fn new_without_copy(
105 bundler: &'a dyn Bundler,
106 template_dir: PathBuf,
107 ) -> Result<PreparedApp<'a>> {
108 Ok(Self {
109 bundler,
110 server: None,
111 test_dir: PreparedDir::Path(template_dir),
112 })
113 }
114
115 pub fn start_server(&mut self) -> Result<()> {
116 assert!(self.server.is_none(), "Server already started");
117
118 self.server = Some(self.bundler.start_server(self.path())?);
119
120 Ok(())
121 }
122
123 pub async fn with_page(self, browser: &Browser) -> Result<PageGuard<'a>> {
124 let server = self.server.as_ref().context("Server must be started")?;
125 let page = browser
126 .new_page("about:blank")
127 .await
128 .context("Unable to open about:blank")?;
129 add_binding(&page)
131 .await
132 .context("Failed to add bindings to the browser tab")?;
133
134 let mut errors = page
135 .event_listener::<EventExceptionThrown>()
136 .await
137 .context("Unable to listen to exception events")?;
138 let binding_events = page
139 .event_listener::<EventBindingCalled>()
140 .await
141 .context("Unable to listen to binding events")?;
142 let mut network_response_events = page
143 .event_listener::<EventResponseReceived>()
144 .await
145 .context("Unable to listen to response received events")?;
146
147 let destination = Url::parse(&server.1)?.join(self.bundler.get_path())?;
148 page.evaluate_expression(format!("window.location='{destination}'"))
155 .await
156 .context("Unable to evaluate javascript to navigate to target page")?;
157
158 loop {
160 match network_response_events.next().await {
161 Some(event) => {
162 if event.response.url == destination.as_str() {
163 break;
164 }
165 }
166 None => return Err(anyhow!("event stream ended too early")),
167 }
168 }
169
170 assert!(errors.next().now_or_never().is_none());
172
173 let page_guard = PageGuard::new(page, binding_events, errors, self);
174
175 Ok(page_guard)
176 }
177
178 pub fn stop_server(&mut self) -> Result<()> {
179 let mut proc = self.server.take().expect("Server never started").0;
180 stop_process(&mut proc)?;
181 Ok(())
182 }
183
184 pub fn path(&self) -> &Path {
185 match self.test_dir {
186 PreparedDir::TempDir(ref dir) => dir.path(),
187 PreparedDir::Path(ref path) => path,
188 }
189 }
190}
191
192impl Drop for PreparedApp<'_> {
193 fn drop(&mut self) {
194 if let Some(mut server) = self.server.take() {
195 stop_process(&mut server.0).expect("failed to stop process");
196 }
197 }
198}
199
200async fn add_binding(page: &Page) -> Result<()> {
202 page.execute(AddBindingParams::new(BINDING_NAME)).await?;
203 Ok(())
204}
205
206#[cfg(unix)]
207fn stop_process(proc: &mut Child) -> Result<()> {
208 use std::time::Duration;
209
210 use nix::{
211 sys::signal::{Signal, kill},
212 unistd::Pid,
213 };
214 use owo_colors::OwoColorize;
215
216 const KILL_DEADLINE: Duration = Duration::from_secs(5);
217 const KILL_DEADLINE_CHECK_STEPS: u32 = 10;
218
219 let pid = Pid::from_raw(proc.id() as _);
220 match kill(pid, Signal::SIGINT) {
221 Ok(()) => {
222 let expire = std::time::Instant::now() + KILL_DEADLINE;
223 while let Ok(None) = proc.try_wait() {
224 if std::time::Instant::now() > expire {
225 break;
226 }
227 std::thread::sleep(KILL_DEADLINE / KILL_DEADLINE_CHECK_STEPS);
228 }
229 if let Ok(None) = proc.try_wait() {
230 eprintln!(
231 "{event_type} - process {pid} did not exit after SIGINT, sending SIGKILL",
232 event_type = "error".red(),
233 pid = pid
234 );
235 kill_process(proc)?;
236 }
237 }
238 Err(_) => {
239 eprintln!(
240 "{event_type} - failed to send SIGINT to process {pid}, sending SIGKILL",
241 event_type = "error".red(),
242 pid = pid
243 );
244 kill_process(proc)?;
245 }
246 }
247 Ok(())
248}
249
250#[cfg(not(unix))]
251fn stop_process(proc: &mut Child) -> Result<()> {
252 kill_process(proc)
253}
254
255fn kill_process(proc: &mut Child) -> Result<()> {
256 proc.kill()?;
257 proc.wait()?;
258 Ok(())
259}