Skip to main content

forge/cmd/test/
evm_profile_server.rs

1//! Local HTTP server for serving EVM profiles to speedscope.app.
2//!
3//! This module implements a temporary local HTTP server that:
4//! 1. Serves the profile JSON at `/{token}/profile.json`
5//! 2. Sets CORS headers to allow the viewer to fetch it
6//! 3. Constructs the proper URL and opens it in the browser
7use 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
24/// Serves a profile on a local HTTP server and opens it in the browser.
25///
26/// Takes the already-serialized profile JSON bytes.
27/// The server runs until Ctrl+C is pressed.
28pub 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    // Run the server until interrupted.
59    axum::serve(listener, app).await?;
60    Ok(())
61}
62
63/// Generates a random token for the URL path (32 hex characters).
64fn 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
72/// Percent-encode a URL for embedding in viewer URL parameters.
73fn 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            // Unreserved characters (RFC 3986).
78            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}