Repository: hatoo/oha
Pull request #297 (2023-09-02 09:20:22)

Refactor Tests


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 9429c90..28c168d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -115,6 +115,55 @@ dependencies = [
  "num-traits",
 ]
 
+[[package]]
+name = "axum"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bitflags 1.3.2",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "mime",
+ "rustversion",
+ "tower-layer",
+ "tower-service",
+]
+
 [[package]]
 name = "backtrace"
 version = "0.3.69"
@@ -130,12 +179,6 @@ dependencies = [
  "rustc-demangle",
 ]
 
-[[package]]
-name = "base64"
-version = "0.13.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
-
 [[package]]
 name = "base64"
 version = "0.21.2"
@@ -154,15 +197,6 @@ version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
 
-[[package]]
-name = "block-buffer"
-version = "0.10.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
-dependencies = [
- "generic-array",
-]
-
 [[package]]
 name = "bstr"
 version = "1.6.0"
@@ -190,12 +224,6 @@ dependencies = [
  "utf8-width",
 ]
 
-[[package]]
-name = "byteorder"
-version = "1.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
-
 [[package]]
 name = "bytes"
 version = "1.4.0"
@@ -286,15 +314,6 @@ version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
 
-[[package]]
-name = "cpufeatures"
-version = "0.2.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
-dependencies = [
- "libc",
-]
-
 [[package]]
 name = "crossterm"
 version = "0.26.1"
@@ -320,16 +339,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "crypto-common"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
-dependencies = [
- "generic-array",
- "typenum",
-]
-
 [[package]]
 name = "data-encoding"
 version = "2.4.0"
@@ -342,16 +351,6 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
 
-[[package]]
-name = "digest"
-version = "0.10.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
-dependencies = [
- "block-buffer",
- "crypto-common",
-]
-
 [[package]]
 name = "doc-comment"
 version = "0.3.3"
@@ -373,15 +372,6 @@ version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
 
-[[package]]
-name = "encoding_rs"
-version = "0.8.32"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
-dependencies = [
- "cfg-if",
-]
-
 [[package]]
 name = "enum-as-inner"
 version = "0.6.0"
@@ -558,16 +548,6 @@ dependencies = [
  "slab",
 ]
 
-[[package]]
-name = "generic-array"
-version = "0.14.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
-dependencies = [
- "typenum",
- "version_check",
-]
-
 [[package]]
 name = "get-port"
 version = "4.0.0"
@@ -618,31 +598,6 @@ version = "0.12.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
 
-[[package]]
-name = "headers"
-version = "0.3.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584"
-dependencies = [
- "base64 0.13.1",
- "bitflags 1.3.2",
- "bytes",
- "headers-core",
- "http",
- "httpdate",
- "mime",
- "sha1",
-]
-
-[[package]]
-name = "headers-core"
-version = "0.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429"
-dependencies = [
- "http",
-]
-
 [[package]]
 name = "heck"
 version = "0.4.1"
@@ -890,6 +845,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
 
+[[package]]
+name = "matchit"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed1202b2a6f884ae56f04cff409ab315c5ce26b5e58d7412e484f01fd52f52ef"
+
 [[package]]
 name = "memchr"
 version = "2.5.0"
@@ -902,16 +863,6 @@ version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
 
-[[package]]
-name = "mime_guess"
-version = "2.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
-dependencies = [
- "mime",
- "unicase",
-]
-
 [[package]]
 name = "miniz_oxide"
 version = "0.7.1"
@@ -933,24 +884,6 @@ dependencies = [
  "windows-sys",
 ]
 
-[[package]]
-name = "multer"
-version = "2.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2"
-dependencies = [
- "bytes",
- "encoding_rs",
- "futures-util",
- "http",
- "httparse",
- "log",
- "memchr",
- "mime",
- "spin 0.9.8",
- "version_check",
-]
-
 [[package]]
 name = "nanorand"
 version = "0.7.0"
@@ -1014,7 +947,8 @@ dependencies = [
  "anyhow",
  "assert_cmd",
  "average",
- "base64 0.21.2",
+ "axum",
+ "base64",
  "byte-unit",
  "bytes",
  "clap",
@@ -1045,7 +979,6 @@ dependencies = [
  "tokio-rustls",
  "trust-dns-resolver",
  "url",
- "warp",
 ]
 
 [[package]]
@@ -1388,7 +1321,7 @@ version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
 dependencies = [
- "base64 0.21.2",
+ "base64",
 ]
 
 [[package]]
@@ -1401,6 +1334,12 @@ dependencies = [
  "untrusted",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
 [[package]]
 name = "ryu"
 version = "1.0.15"
@@ -1416,12 +1355,6 @@ dependencies = [
  "windows-sys",
 ]
 
-[[package]]
-name = "scoped-tls"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
-
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
@@ -1493,26 +1426,25 @@ dependencies = [
 ]
 
 [[package]]
-name = "serde_urlencoded"
-version = "0.7.1"
+name = "serde_path_to_error"
+version = "0.1.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335"
 dependencies = [
- "form_urlencoded",
  "itoa",
- "ryu",
  "serde",
 ]
 
 [[package]]
-name = "sha1"
-version = "0.10.5"
+name = "serde_urlencoded"
+version = "0.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
 dependencies = [
- "cfg-if",
- "cpufeatures",
- "digest",
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
 ]
 
 [[package]]
@@ -1612,6 +1544,12 @@ dependencies = [
  "unicode-ident",
 ]
 
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
 [[package]]
 name = "tempfile"
 version = "3.8.0"
@@ -1717,41 +1655,40 @@ dependencies = [
 ]
 
 [[package]]
-name = "tokio-stream"
-version = "0.1.14"
+name = "tokio-util"
+version = "0.7.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
 dependencies = [
+ "bytes",
  "futures-core",
+ "futures-sink",
  "pin-project-lite",
  "tokio",
+ "tracing",
 ]
 
 [[package]]
-name = "tokio-tungstenite"
-version = "0.18.0"
+name = "tower"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
 dependencies = [
+ "futures-core",
  "futures-util",
- "log",
+ "pin-project",
+ "pin-project-lite",
  "tokio",
- "tungstenite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
 ]
 
 [[package]]
-name = "tokio-util"
-version = "0.7.8"
+name = "tower-layer"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
-dependencies = [
- "bytes",
- "futures-core",
- "futures-sink",
- "pin-project-lite",
- "tokio",
- "tracing",
-]
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
 
 [[package]]
 name = "tower-service"
@@ -1844,40 +1781,6 @@ version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
 
-[[package]]
-name = "tungstenite"
-version = "0.18.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
-dependencies = [
- "base64 0.13.1",
- "byteorder",
- "bytes",
- "http",
- "httparse",
- "log",
- "rand",
- "sha1",
- "thiserror",
- "url",
- "utf-8",
-]
-
-[[package]]
-name = "typenum"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
-
-[[package]]
-name = "unicase"
-version = "2.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
-dependencies = [
- "version_check",
-]
-
 [[package]]
 name = "unicode-bidi"
 version = "0.3.13"
@@ -1928,12 +1831,6 @@ dependencies = [
  "percent-encoding",
 ]
 
-[[package]]
-name = "utf-8"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-
 [[package]]
 name = "utf8-width"
 version = "0.1.6"
@@ -1952,12 +1849,6 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
-[[package]]
-name = "version_check"
-version = "0.9.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
-
 [[package]]
 name = "wait-timeout"
 version = "0.2.0"
@@ -1976,37 +1867,6 @@ dependencies = [
  "try-lock",
 ]
 
-[[package]]
-name = "warp"
-version = "0.3.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ba431ef570df1287f7f8b07e376491ad54f84d26ac473489427231e1718e1f69"
-dependencies = [
- "bytes",
- "futures-channel",
- "futures-util",
- "headers",
- "http",
- "hyper",
- "log",
- "mime",
- "mime_guess",
- "multer",
- "percent-encoding",
- "pin-project",
- "rustls-pemfile",
- "scoped-tls",
- "serde",
- "serde_json",
- "serde_urlencoded",
- "tokio",
- "tokio-stream",
- "tokio-tungstenite",
- "tokio-util",
- "tower-service",
- "tracing",
-]
-
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
diff --git a/Cargo.toml b/Cargo.toml
index d8cd67e..7965d72 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -69,8 +69,8 @@ jemallocator = "0.5.0"
 
 [dev-dependencies]
 assert_cmd = "2.0.2"
+axum = "0.6.20"
 bytes = "1.0"
 get-port = "4.0.0"
 http = "0.2"
 lazy_static = "1.4.0"
-warp = "0.3"


Test patch:
diff --git a/tests/tests.rs b/tests/tests.rs
index 6ccc144..2abbfdc 100644
--- a/tests/tests.rs
+++ b/tests/tests.rs
@@ -1,29 +1,65 @@
-use std::{net::Ipv6Addr, sync::Arc};
+use std::{
+    convert::Infallible,
+    net::{Ipv6Addr, SocketAddr},
+    sync::{Mutex, OnceLock},
+};
 
 use assert_cmd::Command;
+use axum::{
+    extract::{Path, RawBody},
+    response::Redirect,
+    routing::{any, get},
+    Router,
+};
 use get_port::Ops;
-use tokio::sync::Mutex;
-use warp::{http::HeaderMap, Filter};
+use http::{HeaderMap, Response};
+use hyper::{
+    body::to_bytes,
+    server::conn::AddrIncoming,
+    service::{make_service_fn, service_fn},
+    Body,
+};
 
-lazy_static::lazy_static! {
-    static ref PORT_LOCK: Mutex<()> = Mutex::new(());
+static PORT_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
+
+// get_port only check ipv4 port and it seems that port for ipv6 is independent to ipv4
+// so we chose to serialize runs for ipv6
+static IPV6_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
+
+fn bind_port() -> (hyper::server::Builder<AddrIncoming>, u16) {
+    let _guard = PORT_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
+
+    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
+    let addr = SocketAddr::new("127.0.0.1".parse().unwrap(), port);
+
+    (axum::Server::bind(&addr), port)
+}
+
+fn bind_port_ipv6() -> (hyper::server::Builder<AddrIncoming>, u16) {
+    let _guard = PORT_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
+
+    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
+    let addr = SocketAddr::new(std::net::IpAddr::V6(Ipv6Addr::LOCALHOST), port);
+
+    (axum::Server::bind(&addr), port)
 }
 
 async fn get_header_body(args: &[&str]) -> (HeaderMap, bytes::Bytes) {
     let (tx, rx) = flume::unbounded();
-    let report_headers = warp::any()
-        .and(warp::header::headers_cloned())
-        .and(warp::filters::body::bytes())
-        .map(move |headers: HeaderMap, body: bytes::Bytes| {
-            tx.send((headers, body)).unwrap();
+
+    let app = Router::new().route(
+        "/",
+        get(|header: HeaderMap, RawBody(body): RawBody| async move {
+            tx.send((header, to_bytes(body).await.unwrap())).unwrap();
             "Hello World"
-        });
+        }),
+    );
 
-    let _guard = PORT_LOCK.lock().await;
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    tokio::spawn(warp::serve(report_headers).run(([127, 0, 0, 1], port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let (server, port) = bind_port();
+
+    tokio::spawn(async {
+        server.serve(app.into_make_service()).await.unwrap();
+    });
 
     let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
     tokio::task::spawn_blocking(move || {
@@ -43,20 +79,22 @@ async fn get_header_body(args: &[&str]) -> (HeaderMap, bytes::Bytes) {
 
 async fn get_method(args: &[&str]) -> http::method::Method {
     let (tx, rx) = flume::unbounded();
-    let report_headers = warp::any().and(warp::filters::method::method()).map(
-        move |method: http::method::Method| {
+    let app = Router::new().route(
+        "/",
+        any(|method: http::method::Method| async move {
             tx.send(method).unwrap();
             "Hello World"
-        },
+        }),
     );
 
-    let _guard = PORT_LOCK.lock().await;
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    tokio::spawn(warp::serve(report_headers).run(([127, 0, 0, 1], port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let (server, port) = bind_port();
+
+    tokio::spawn(async {
+        server.serve(app.into_make_service()).await.unwrap();
+    });
 
     let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
+
     tokio::task::spawn_blocking(move || {
         Command::cargo_bin("oha")
             .unwrap()
@@ -73,25 +111,34 @@ async fn get_method(args: &[&str]) -> http::method::Method {
 }
 
 async fn get_query(p: &'static str) -> String {
+    use hyper::Error;
     let (tx, rx) = flume::unbounded();
-    let report_headers = warp::path!(String).and(warp::filters::query::raw()).map(
-        move |path: String, query: String| {
-            tx.send(path + "?" + &query).unwrap();
-            "Hello World"
-        },
-    );
 
-    let _guard = PORT_LOCK.lock().await;
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    tokio::spawn(warp::serve(report_headers).run(([127, 0, 0, 1], port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let make_svc = make_service_fn(move |_| {
+        let tx = tx.clone();
+        async move {
+            Ok::<_, Error>(service_fn(move |req| {
+                let tx = tx.clone();
+                async move {
+                    let (parts, _) = req.into_parts();
+                    tx.send(parts.uri.to_string()).unwrap();
+                    Ok::<_, Error>(Response::new(Body::from("Hello World")))
+                }
+            }))
+        }
+    });
+
+    let (server, port) = bind_port();
+
+    tokio::spawn(async move {
+        server.serve(make_svc).await.unwrap();
+    });
 
     tokio::task::spawn_blocking(move || {
         Command::cargo_bin("oha")
             .unwrap()
             .args(["-n", "1", "--no-tui"])
-            .arg(format!("http://127.0.0.1:{port}/{p}"))
+            .arg(format!("http://127.0.0.1:{port}{p}"))
             .assert()
             .success();
     })
@@ -102,23 +149,34 @@ async fn get_query(p: &'static str) -> String {
 }
 
 async fn get_path_rand_regex(p: &'static str) -> String {
+    use hyper::Error;
+
     let (tx, rx) = flume::unbounded();
-    let report_headers = warp::path!(String).map(move |path: String| {
-        tx.send(path).unwrap();
-        "Hello World"
+    let make_svc = make_service_fn(move |_| {
+        let tx = tx.clone();
+        async move {
+            Ok::<_, Error>(service_fn(move |req| {
+                let tx = tx.clone();
+                async move {
+                    let (parts, _) = req.into_parts();
+                    tx.send(parts.uri.to_string()).unwrap();
+                    Ok::<_, Error>(Response::new(Body::from("Hello World")))
+                }
+            }))
+        }
     });
 
-    let _guard = PORT_LOCK.lock().await;
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    tokio::spawn(warp::serve(report_headers).run(([127, 0, 0, 1], port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let (server, port) = bind_port();
+
+    tokio::spawn(async move {
+        server.serve(make_svc).await.unwrap();
+    });
 
     tokio::task::spawn_blocking(move || {
         Command::cargo_bin("oha")
             .unwrap()
             .args(["-n", "1", "--no-tui", "--rand-regex-url"])
-            .arg(format!(r"http://127.0.0.1:{port}/{p}"))
+            .arg(format!(r"http://127.0.0.1:{port}{p}"))
             .assert()
             .success();
     })
@@ -130,31 +188,26 @@ async fn get_path_rand_regex(p: &'static str) -> String {
 
 async fn redirect(n: usize, is_relative: bool, limit: usize) -> bool {
     let (tx, rx) = flume::unbounded();
-    let _guard = PORT_LOCK.lock().await;
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
 
-    let route = warp::path!(usize).map(move |x| {
-        if x == n {
-            tx.send(()).unwrap();
-            http::Response::builder().status(200).body("OK").unwrap()
-        } else if is_relative {
-            http::Response::builder()
-                .status(301)
-                .header("Location", format!("/{}", x + 1))
-                .body("OK")
-                .unwrap()
-        } else {
-            http::Response::builder()
-                .status(301)
-                .header("Location", format!("http://localhost:{}/{}", port, x + 1))
-                .body("OK")
-                .unwrap()
-        }
-    });
+    let (server, port) = bind_port();
+
+    let app = Router::new().route(
+        "/:n",
+        get(move |Path(x): Path<usize>| async move {
+            Ok::<_, Infallible>(if x == n {
+                tx.send(()).unwrap();
+                Redirect::permanent("/end")
+            } else if is_relative {
+                Redirect::permanent(&format!("/{}", x + 1))
+            } else {
+                Redirect::permanent(&format!("http://localhost:{}/{}", port, x + 1))
+            })
+        }),
+    );
 
-    tokio::spawn(warp::serve(route).run(([127, 0, 0, 1], port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    tokio::spawn(async move {
+        server.serve(app.into_make_service()).await.unwrap();
+    });
 
     tokio::task::spawn_blocking(move || {
         Command::cargo_bin("oha")
@@ -173,19 +226,20 @@ async fn redirect(n: usize, is_relative: bool, limit: usize) -> bool {
 
 async fn get_host_with_connect_to(host: &'static str) -> String {
     let (tx, rx) = flume::unbounded();
-    let report_host =
-        warp::get()
-            .and(warp::filters::header::header("host"))
-            .map(move |host: String| {
-                tx.send(host).unwrap();
-                "Hello World"
-            });
 
-    let _guard = PORT_LOCK.lock().await;
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    tokio::spawn(warp::serve(report_host).run(([127, 0, 0, 1], port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let app = Router::new().route(
+        "/",
+        get(|header: HeaderMap| async move {
+            tx.send(header.get("host").unwrap().to_str().unwrap().to_string())
+                .unwrap();
+            "Hello World"
+        }),
+    );
+
+    let (server, port) = bind_port();
+    tokio::spawn(async move {
+        server.serve(app.into_make_service()).await.unwrap();
+    });
 
     tokio::task::spawn_blocking(move || {
         Command::cargo_bin("oha")
@@ -205,22 +259,20 @@ async fn get_host_with_connect_to(host: &'static str) -> String {
 
 async fn get_host_with_connect_to_ipv6_target(host: &'static str) -> String {
     let (tx, rx) = flume::unbounded();
-    let report_host =
-        warp::get()
-            .and(warp::filters::header::header("host"))
-            .map(move |host: String| {
-                tx.send(host).unwrap();
-                "Hello World"
-            });
+    let app = Router::new().route(
+        "/",
+        get(|header: HeaderMap| async move {
+            tx.send(header.get("host").unwrap().to_str().unwrap().to_string())
+                .unwrap();
+            "Hello World"
+        }),
+    );
 
-    let _guard = PORT_LOCK.lock().await;
-    // sic. the `get_port` crate doesn't support IpV6 addresses, so we check
-    // with 127.0.0.1 even though we bind on ::1 later.
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    let addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1);
-    tokio::spawn(warp::serve(report_host).run((addr, port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let _guard = IPV6_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
+    let (server, port) = bind_port_ipv6();
+    tokio::spawn(async move {
+        server.serve(app.into_make_service()).await.unwrap();
+    });
 
     tokio::task::spawn_blocking(move || {
         Command::cargo_bin("oha")
@@ -228,7 +280,7 @@ async fn get_host_with_connect_to_ipv6_target(host: &'static str) -> String {
             .args(["-n", "1", "--no-tui"])
             .arg(format!("http://{host}/"))
             .arg("--connect-to")
-            .arg(format!("{host}:80:[{addr}]:{port}"))
+            .arg(format!("{host}:80:[::1]:{port}"))
             .assert()
             .success();
     })
@@ -240,21 +292,19 @@ async fn get_host_with_connect_to_ipv6_target(host: &'static str) -> String {
 
 async fn get_host_with_connect_to_ipv6_requested() -> String {
     let (tx, rx) = flume::unbounded();
-    let report_host =
-        warp::get()
-            .and(warp::filters::header::header("host"))
-            .map(move |host: String| {
-                tx.send(host).unwrap();
-                "Hello World"
-            });
+    let app = Router::new().route(
+        "/",
+        get(|header: HeaderMap| async move {
+            tx.send(header.get("host").unwrap().to_str().unwrap().to_string())
+                .unwrap();
+            "Hello World"
+        }),
+    );
 
-    let _guard = PORT_LOCK.lock().await;
-    // sic. the `get_port` crate doesn't support IpV6 addresses, so we check
-    // with 127.0.0.1 even though we bind on ::1 later.
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    tokio::spawn(warp::serve(report_host).run(([127, 0, 0, 1], port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let (server, port) = bind_port();
+    tokio::spawn(async move {
+        server.serve(app.into_make_service()).await.unwrap();
+    });
 
     tokio::task::spawn_blocking(move || {
         Command::cargo_bin("oha")
@@ -274,24 +324,24 @@ async fn get_host_with_connect_to_ipv6_requested() -> String {
 
 async fn get_host_with_connect_to_redirect(host: &'static str) -> String {
     let (tx, rx) = flume::unbounded();
-    let redirect = warp::get().and(warp::path!("source")).map(move || {
-        let uri = http::Uri::try_from(format!("http://{host}/destination")).unwrap();
-        warp::redirect(uri)
-    });
-    let report_host = warp::get()
-        .and(warp::path!("destination"))
-        .and(warp::filters::header::header("host"))
-        .map(move |host: String| {
-            tx.send(host).unwrap();
-            "Hello World"
-        });
-    let routes = redirect.or(report_host);
 
-    let _guard = PORT_LOCK.lock().await;
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    tokio::spawn(warp::serve(routes).run(([127, 0, 0, 1], port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let app = Router::new()
+        .route(
+            "/source",
+            get(move || async move { Redirect::permanent(&format!("http://{host}/destination")) }),
+        )
+        .route(
+            "/destination",
+            get(move || async move {
+                tx.send(host.to_string()).unwrap();
+                "Hello World"
+            }),
+        );
+
+    let (server, port) = bind_port();
+    tokio::spawn(async move {
+        server.serve(app.into_make_service()).await.unwrap();
+    });
 
     tokio::task::spawn_blocking(move || {
         Command::cargo_bin("oha")
@@ -309,25 +359,21 @@ async fn get_host_with_connect_to_redirect(host: &'static str) -> String {
     rx.try_recv().unwrap()
 }
 
-async fn burst_10_req_delay_2s_rate_4(
-    iteration: u8,
-    args: &[&str],
-    responses: &Arc<Mutex<Vec<String>>>,
-) {
+async fn burst_10_req_delay_2s_rate_4(iteration: u8, args: &[&str]) -> usize {
     let (tx, rx) = flume::unbounded();
-    let report_requests = warp::any()
-        .and(warp::header::headers_cloned())
-        .and(warp::filters::body::bytes())
-        .map(move |_: HeaderMap, _: bytes::Bytes| {
-            tx.send("Success".into()).unwrap();
+
+    let app = Router::new().route(
+        "/",
+        get(|| async move {
+            tx.send(()).unwrap();
             "Success"
-        });
+        }),
+    );
 
-    let _guard = PORT_LOCK.lock().await;
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    tokio::spawn(warp::serve(report_requests).run(([127, 0, 0, 1], port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let (service, port) = bind_port();
+    tokio::spawn(async move {
+        service.serve(app.into_make_service()).await.unwrap();
+    });
 
     let args: Vec<String> = args.iter().map(|s| s.to_string()).collect();
     tokio::task::spawn_blocking(move || {
@@ -350,13 +396,11 @@ async fn burst_10_req_delay_2s_rate_4(
     .await
     .unwrap();
 
-    let responses_clone = Arc::clone(responses);
-    let receiver = tokio::spawn(async move {
-        for _ in 0..iteration {
-            responses_clone.lock().await.push(rx.try_recv().unwrap())
-        }
-    });
-    receiver.await.expect("Failed to receive responses");
+    let mut count = 0;
+    while let Ok(()) = rx.try_recv() {
+        count += 1;
+    }
+    count
 }
 
 #[tokio::test]
@@ -459,21 +503,33 @@ async fn test_setting_method() {
 #[tokio::test]
 async fn test_query() {
     assert_eq!(
-        get_query("index?a=b&c=d").await,
-        "index?a=b&c=d".to_string()
+        get_query("/index?a=b&c=d").await,
+        "/index?a=b&c=d".to_string()
     );
 }
 
 #[tokio::test]
 async fn test_query_rand_regex() {
-    let query = get_path_rand_regex("[a-z][0-9][a-z]").await;
-    let chars = query.chars().collect::<Vec<char>>();
+    let query = get_path_rand_regex("/[a-z][0-9][a-z]").await;
+    let chars = query.trim_start_matches('/').chars().collect::<Vec<char>>();
     assert_eq!(chars.len(), 3);
     assert!(chars[0].is_ascii_lowercase());
     assert!(chars[1].is_ascii_digit());
     assert!(chars[2].is_ascii_lowercase());
 }
 
+#[tokio::test]
+async fn test_redirect() {
+    for n in 1..=5 {
+        assert!(redirect(n, true, 10).await);
+        assert!(redirect(n, false, 10).await);
+    }
+    for n in 11..=15 {
+        assert!(!redirect(n, true, 10).await);
+        assert!(!redirect(n, false, 10).await);
+    }
+}
+
 #[tokio::test]
 async fn test_connect_to() {
     assert_eq!(
@@ -503,37 +559,23 @@ async fn test_connect_to_redirect() {
     )
 }
 
-#[tokio::test]
-async fn test_redirect() {
-    for n in 1..=5 {
-        assert!(redirect(n, true, 10).await);
-        assert!(redirect(n, false, 10).await);
-    }
-    for n in 11..=15 {
-        assert!(!redirect(n, true, 10).await);
-        assert!(!redirect(n, false, 10).await);
-    }
-}
-
 #[tokio::test]
 async fn test_ipv6() {
     let (tx, rx) = flume::unbounded();
-    let report_host =
-        warp::get()
-            .and(warp::filters::header::header("host"))
-            .map(move |host: String| {
-                tx.send(host).unwrap();
-                "Hello World"
-            });
 
-    let _guard = PORT_LOCK.lock().await;
-    // sic. the `get_port` crate doesn't support IpV6 addresses, so we check
-    // with 127.0.0.1 even though we bind on ::1 later.
-    let port = get_port::tcp::TcpPort::any("127.0.0.1").unwrap();
-    let addr = Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1);
-    tokio::spawn(warp::serve(report_host).run((addr, port)));
-    // It's not guaranteed that the port is used here.
-    // So we can't drop guard here.
+    let app = Router::new().route(
+        "/",
+        get(|| async move {
+            tx.send(()).unwrap();
+            "Hello World"
+        }),
+    );
+
+    let _guard = IPV6_LOCK.get_or_init(|| Mutex::new(())).lock().unwrap();
+    let (service, port) = bind_port_ipv6();
+    tokio::spawn(async move {
+        service.serve(app.into_make_service()).await.unwrap();
+    });
 
     tokio::task::spawn_blocking(move || {
         Command::cargo_bin("oha")
@@ -551,7 +593,5 @@ async fn test_ipv6() {
 
 #[tokio::test]
 async fn test_query_limit() {
-    let responses: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
-    let _ = burst_10_req_delay_2s_rate_4(10, &[], &responses).await;
-    assert_eq!(responses.lock().await.len(), 10);
+    assert_eq!(burst_10_req_delay_2s_rate_4(10, &[],).await, 10);
 }