forge/cmd/test/
evm_profile_server.rs1use axum::{
8 Router,
9 body::Bytes,
10 extract::State,
11 http::{Method, StatusCode, header},
12 response::{IntoResponse, Response},
13 routing::get,
14};
15use eyre::Result;
16use foundry_common::{sh_err, sh_println};
17use std::{
18 hash::{DefaultHasher, Hasher},
19 time::{SystemTime, UNIX_EPOCH},
20};
21use tokio::net::TcpListener;
22use tower_http::cors::{Any, CorsLayer};
23
24pub async fn serve_and_open(
29 profile_json: Vec<u8>,
30 test_name: &str,
31 contract_name: &str,
32) -> Result<()> {
33 let token = generate_token();
34 let app = Router::new()
35 .route(&format!("/{token}/profile.json"), get(serve_profile))
36 .layer(
37 CorsLayer::new()
38 .allow_origin(Any)
39 .allow_methods([Method::GET, Method::OPTIONS])
40 .allow_headers(Any),
41 )
42 .with_state(Bytes::from(profile_json));
43
44 let listener = TcpListener::bind("127.0.0.1:0").await?;
45 let port = listener.local_addr()?.port();
46
47 let profile_url = percent_encode(&format!("http://127.0.0.1:{port}/{token}/profile.json"));
48 let title = percent_encode(&format!("{contract_name}::{test_name}"));
49 let viewer_url = format!("https://www.speedscope.app/#profileURL={profile_url}&title={title}");
50
51 sh_println!("Profile server running at http://127.0.0.1:{port}")?;
52 sh_println!("Opening speedscope: {viewer_url}")?;
53 if let Err(e) = opener::open(&viewer_url) {
54 sh_err!("Failed to open browser: {e}")?;
55 }
56 sh_println!("\nPress Ctrl+C to stop the server.")?;
57
58 axum::serve(listener, app).await?;
60 Ok(())
61}
62
63fn generate_token() -> String {
65 let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
66 let mut hasher = DefaultHasher::new();
67 hasher.write_u128(nanos);
68 hasher.write_usize(std::process::id() as usize);
69 format!("{nanos:016x}{:016x}", hasher.finish())
70}
71
72fn percent_encode(url: &str) -> String {
74 let mut result = String::with_capacity(url.len() * 3);
75 for byte in url.bytes() {
76 match byte {
77 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
79 result.push(byte as char)
80 }
81 _ => result.push_str(&format!("%{byte:02X}")),
82 }
83 }
84 result
85}
86
87async fn serve_profile(State(profile_json): State<Bytes>) -> Response {
88 (StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], profile_json).into_response()
89}