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 tokio::net::TcpListener;
18use tower_http::cors::{Any, CorsLayer};
19
20pub async fn serve_and_open(
25 profile_json: Vec<u8>,
26 test_name: &str,
27 contract_name: &str,
28) -> Result<()> {
29 let token = generate_token();
30
31 let state = ServerState { profile_json: Bytes::from(profile_json) };
32
33 let app = Router::new()
34 .route(&format!("/{token}/profile.json"), get(serve_profile))
35 .layer(
36 CorsLayer::new()
37 .allow_origin(Any)
38 .allow_methods([Method::GET, Method::OPTIONS])
39 .allow_headers(Any),
40 )
41 .with_state(state);
42
43 let bind_addr = "127.0.0.1:0";
44
45 let listener = TcpListener::bind(bind_addr).await?;
46 let port = listener.local_addr()?.port();
47
48 let profile_url = format!("http://127.0.0.1:{port}/{token}/profile.json");
49 let title = format!("{contract_name}::{test_name}");
50
51 let encoded_url = percent_encode(&profile_url);
52 let encoded_title = percent_encode(&title);
53 let viewer_url =
54 format!("https://www.speedscope.app/#profileURL={encoded_url}&title={encoded_title}");
55
56 sh_println!("Profile server running at http://127.0.0.1:{port}")?;
57
58 sh_println!("Opening speedscope: {viewer_url}")?;
59 if let Err(e) = opener::open(&viewer_url) {
60 sh_err!("Failed to open browser: {e}")?;
61 }
62
63 sh_println!("\nPress Ctrl+C to stop the server.")?;
64
65 axum::serve(listener, app).await?;
67
68 Ok(())
69}
70
71fn generate_token() -> String {
73 use std::{
74 hash::{DefaultHasher, Hasher},
75 time::{SystemTime, UNIX_EPOCH},
76 };
77
78 let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
79 let mut hasher = DefaultHasher::new();
80 hasher.write_u128(nanos);
81 hasher.write_usize(std::process::id() as usize);
82 let random_part = hasher.finish();
83 format!("{nanos:016x}{random_part:016x}")
84}
85
86fn percent_encode(url: &str) -> String {
88 let mut result = String::with_capacity(url.len() * 3);
89 for byte in url.bytes() {
90 match byte {
91 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
93 result.push(byte as char);
94 }
95 _ => {
97 result.push('%');
98 result.push_str(&format!("{byte:02X}"));
99 }
100 }
101 }
102 result
103}
104
105#[derive(Clone)]
106struct ServerState {
107 profile_json: Bytes,
108}
109
110async fn serve_profile(State(state): State<ServerState>) -> Response {
111 (StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], state.profile_json)
112 .into_response()
113}