Skip to content

Commit 60f3cd5

Browse files
committed
Add end-to-end testbench.
Resolves #1509.
1 parent eec5c08 commit 60f3cd5

File tree

8 files changed

+346
-1
lines changed

8 files changed

+346
-1
lines changed

.github/workflows/ci.yml

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ jobs:
2828
test: { name: Core, flag: "--core" }
2929
- platform: { name: Linux, distro: ubuntu-latest, toolchain: stable }
3030
test: { name: Release, flag: "--release" }
31+
- platform: { name: Linux, distro: ubuntu-latest, toolchain: stable }
32+
test: { name: Testbench, flag: "--testbench" }
3133
- platform: { name: Linux, distro: ubuntu-latest, toolchain: stable }
3234
test: { name: UI, flag: "--ui" }
3335
fallible: true

core/http/src/parse/uri/error.rs

+2
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ impl IntoOwned for Error<'_> {
6161
}
6262
}
6363

64+
impl std::error::Error for Error<'_> { }
65+
6466
#[cfg(test)]
6567
mod tests {
6668
use crate::parse::uri::origin_from_str;

scripts/config.sh

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ function future_date() {
3939
PROJECT_ROOT=$(relative "") || exit $?
4040
CONTRIB_ROOT=$(relative "contrib") || exit $?
4141
BENCHMARKS_ROOT=$(relative "benchmarks") || exit $?
42+
TESTBENCH_ROOT=$(relative "testbench") || exit $?
4243
FUZZ_ROOT=$(relative "core/lib/fuzz") || exit $?
4344

4445
# Root of project-like directories.
@@ -87,6 +88,7 @@ function print_environment() {
8788
echo " CONTRIB_ROOT: ${CONTRIB_ROOT}"
8889
echo " FUZZ_ROOT: ${FUZZ_ROOT}"
8990
echo " BENCHMARKS_ROOT: ${BENCHMARKS_ROOT}"
91+
echo " TESTBENCH_ROOT: ${TESTBENCH_ROOT}"
9092
echo " CORE_LIB_ROOT: ${CORE_LIB_ROOT}"
9193
echo " CORE_CODEGEN_ROOT: ${CORE_CODEGEN_ROOT}"
9294
echo " CORE_HTTP_ROOT: ${CORE_HTTP_ROOT}"

scripts/test.sh

+10-1
Original file line numberDiff line numberDiff line change
@@ -184,14 +184,20 @@ function run_benchmarks() {
184184
indir "${BENCHMARKS_ROOT}" $CARGO bench $@
185185
}
186186

187+
function run_testbench() {
188+
echo ":: Running testbench..."
189+
indir "${TESTBENCH_ROOT}" $CARGO update
190+
indir "${TESTBENCH_ROOT}" $CARGO run $@
191+
}
192+
187193
if [[ $1 == +* ]]; then
188194
CARGO="$CARGO $1"
189195
shift
190196
fi
191197

192198
# The kind of test we'll be running.
193199
TEST_KIND="default"
194-
KINDS=("contrib" "benchmarks" "core" "examples" "default" "ui" "all")
200+
KINDS=("contrib" "benchmarks" "testbench" "core" "examples" "default" "ui" "all")
195201

196202
if [[ " ${KINDS[@]} " =~ " ${1#"--"} " ]]; then
197203
TEST_KIND=${1#"--"}
@@ -226,19 +232,22 @@ case $TEST_KIND in
226232
examples) test_examples $@ ;;
227233
default) test_default $@ ;;
228234
benchmarks) run_benchmarks $@ ;;
235+
testbench) run_testbench $@ ;;
229236
ui) test_ui $@ ;;
230237
all)
231238
test_default $@ & default=$!
232239
test_examples $@ & examples=$!
233240
test_core $@ & core=$!
234241
test_contrib $@ & contrib=$!
242+
run_testbench $@ & testbench=$!
235243
test_ui $@ & ui=$!
236244

237245
failures=()
238246
if ! wait $default ; then failures+=("DEFAULT"); fi
239247
if ! wait $examples ; then failures+=("EXAMPLES"); fi
240248
if ! wait $core ; then failures+=("CORE"); fi
241249
if ! wait $contrib ; then failures+=("CONTRIB"); fi
250+
if ! wait $testbench ; then failures+=("TESTBENCH"); fi
242251
if ! wait $ui ; then failures+=("UI"); fi
243252

244253
if [ ${#failures[@]} -ne 0 ]; then

testbench/Cargo.toml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
[package]
2+
name = "rocket-testbench"
3+
description = "end-to-end HTTP testbench for Rocket"
4+
version = "0.0.0"
5+
edition = "2021"
6+
publish = false
7+
8+
[workspace]
9+
10+
[dependencies]
11+
thiserror = "1.0"
12+
procspawn = "1"
13+
pretty_assertions = "1.4.0"
14+
ipc-channel = "0.18"
15+
16+
[dependencies.nix]
17+
version = "0.28"
18+
features = ["signal"]
19+
20+
[dependencies.rocket]
21+
path = "../core/lib/"
22+
features = ["secrets", "tls", "mtls"]
23+
24+
[dependencies.reqwest]
25+
version = "0.12.3"
26+
default-features = false
27+
features = ["rustls-tls-manual-roots", "charset", "cookies", "blocking", "http2"]

testbench/src/client.rs

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
use std::time::Duration;
2+
use std::sync::Once;
3+
use std::process::Stdio;
4+
use std::io::{self, Read};
5+
6+
use rocket::fairing::AdHoc;
7+
use rocket::http::ext::IntoOwned;
8+
use rocket::http::uri::{self, Absolute, Uri};
9+
use rocket::serde::{Deserialize, Serialize};
10+
use rocket::{Build, Rocket};
11+
12+
use procspawn::SpawnError;
13+
use thiserror::Error;
14+
use ipc_channel::ipc::{IpcOneShotServer, IpcReceiver, IpcSender};
15+
16+
static DEFAULT_CONFIG: &str = r#"
17+
[default]
18+
address = "tcp:127.0.0.1"
19+
workers = 2
20+
port = 0
21+
cli_colors = false
22+
secret_key = "itlYmFR2vYKrOmFhupMIn/hyB6lYCCTXz4yaQX89XVg="
23+
24+
[default.shutdown]
25+
grace = 1
26+
mercy = 1
27+
"#;
28+
29+
#[derive(Debug)]
30+
#[allow(unused)]
31+
pub struct Client {
32+
client: reqwest::blocking::Client,
33+
server: procspawn::JoinHandle<()>,
34+
tls: bool,
35+
port: u16,
36+
rx: IpcReceiver<Message>,
37+
}
38+
39+
#[derive(Error, Debug)]
40+
pub enum Error {
41+
#[error("join/kill failed: {0}")]
42+
JoinError(#[from] SpawnError),
43+
#[error("kill failed: {0}")]
44+
TermFailure(#[from] nix::errno::Errno),
45+
#[error("i/o error: {0}")]
46+
Io(#[from] io::Error),
47+
#[error("invalid URI: {0}")]
48+
Uri(#[from] uri::Error<'static>),
49+
#[error("the URI is invalid")]
50+
InvalidUri,
51+
#[error("bad request: {0}")]
52+
Request(#[from] reqwest::Error),
53+
#[error("IPC failure: {0}")]
54+
Ipc(#[from] ipc_channel::ipc::IpcError),
55+
#[error("liftoff failed")]
56+
Liftoff(String, String),
57+
}
58+
59+
#[derive(Debug, Serialize, Deserialize)]
60+
#[serde(crate = "rocket::serde")]
61+
pub enum Message {
62+
Liftoff(bool, u16),
63+
Failure,
64+
}
65+
66+
#[derive(Serialize, Deserialize)]
67+
#[serde(crate = "rocket::serde")]
68+
#[must_use]
69+
pub struct Token(String);
70+
71+
pub type Result<T, E = Error> = std::result::Result<T, E>;
72+
73+
impl Token {
74+
fn configure(&self, toml: &str, rocket: Rocket<Build>) -> Rocket<Build> {
75+
use rocket::figment::{Figment, providers::{Format, Toml}};
76+
77+
let toml = toml.replace("{CRATE}", env!("CARGO_MANIFEST_DIR"));
78+
let config = Figment::from(rocket.figment())
79+
.merge(Toml::string(DEFAULT_CONFIG).nested())
80+
.merge(Toml::string(&toml).nested());
81+
82+
let server = self.0.clone();
83+
rocket.configure(config)
84+
.attach(AdHoc::on_liftoff("Liftoff", move |rocket| Box::pin(async move {
85+
let tcp = rocket.endpoints().find_map(|e| e.tcp()).unwrap();
86+
let tls = rocket.endpoints().any(|e| e.is_tls());
87+
let sender = IpcSender::<Message>::connect(server).unwrap();
88+
let _ = sender.send(Message::Liftoff(tls, tcp.port()));
89+
let _ = sender.send(Message::Liftoff(tls, tcp.port()));
90+
})))
91+
}
92+
93+
pub fn rocket(&self, toml: &str) -> Rocket<Build> {
94+
self.configure(toml, rocket::build())
95+
}
96+
97+
pub fn configured_launch(self, toml: &str, rocket: Rocket<Build>) {
98+
let rocket = self.configure(toml, rocket);
99+
if let Err(e) = rocket::execute(rocket.launch()) {
100+
let sender = IpcSender::<Message>::connect(self.0).unwrap();
101+
let _ = sender.send(Message::Failure);
102+
let _ = sender.send(Message::Failure);
103+
e.pretty_print();
104+
std::process::exit(1);
105+
}
106+
}
107+
108+
pub fn launch(self, rocket: Rocket<Build>) {
109+
self.configured_launch(DEFAULT_CONFIG, rocket)
110+
}
111+
}
112+
pub fn start(f: fn(Token)) -> Result<Client> {
113+
static INIT: Once = Once::new();
114+
INIT.call_once(procspawn::init);
115+
116+
let (ipc, server) = IpcOneShotServer::new()?;
117+
let mut server = procspawn::Builder::new()
118+
.stdin(Stdio::null())
119+
.stdout(Stdio::piped())
120+
.stderr(Stdio::piped())
121+
.spawn(Token(server), f);
122+
123+
let client = reqwest::blocking::Client::builder()
124+
.danger_accept_invalid_certs(true)
125+
.cookie_store(true)
126+
.tls_info(true)
127+
.timeout(Duration::from_secs(5))
128+
.connect_timeout(Duration::from_secs(5))
129+
.build()?;
130+
131+
let (rx, _) = ipc.accept().unwrap();
132+
match rx.recv() {
133+
Ok(Message::Liftoff(tls, port)) => Ok(Client { client, server, tls, port, rx }),
134+
Ok(Message::Failure) => {
135+
let stdout = server.stdout().unwrap();
136+
let mut out = String::new();
137+
stdout.read_to_string(&mut out)?;
138+
139+
let stderr = server.stderr().unwrap();
140+
let mut err = String::new();
141+
stderr.read_to_string(&mut err)?;
142+
Err(Error::Liftoff(out, err))
143+
}
144+
Err(e) => Err(e.into()),
145+
}
146+
147+
}
148+
149+
pub fn default() -> Result<Client> {
150+
start(|token| token.launch(rocket::build()))
151+
}
152+
153+
impl Client {
154+
pub fn read_stdout(&mut self) -> Result<String> {
155+
let Some(stdout) = self.server.stdout() else {
156+
return Ok(String::new());
157+
};
158+
159+
let mut string = String::new();
160+
stdout.read_to_string(&mut string)?;
161+
Ok(string)
162+
}
163+
164+
pub fn read_stderr(&mut self) -> Result<String> {
165+
let Some(stderr) = self.server.stderr() else {
166+
return Ok(String::new());
167+
};
168+
169+
let mut string = String::new();
170+
stderr.read_to_string(&mut string)?;
171+
Ok(string)
172+
}
173+
174+
pub fn kill(&mut self) -> Result<()> {
175+
Ok(self.server.kill()?)
176+
}
177+
178+
pub fn terminate(&mut self) -> Result<()> {
179+
use nix::{sys::signal, unistd::Pid};
180+
181+
let pid = Pid::from_raw(self.server.pid().unwrap() as i32);
182+
Ok(signal::kill(pid, signal::SIGTERM)?)
183+
}
184+
185+
pub fn wait(&mut self) -> Result<()> {
186+
match self.server.join_timeout(Duration::from_secs(5)) {
187+
Ok(_) => Ok(()),
188+
Err(e) if e.is_remote_close() => Ok(()),
189+
Err(e) => Err(e.into()),
190+
}
191+
}
192+
193+
pub fn get(&self, url: &str) -> Result<reqwest::blocking::RequestBuilder> {
194+
let uri = match Uri::parse_any(url).map_err(|e| e.into_owned())? {
195+
Uri::Origin(uri) => {
196+
let proto = if self.tls { "https" } else { "http" };
197+
let uri = format!("{proto}://127.0.0.1:{}{uri}", self.port);
198+
Absolute::parse_owned(uri)?
199+
}
200+
Uri::Absolute(uri) => uri,
201+
_ => return Err(Error::InvalidUri),
202+
};
203+
204+
Ok(self.client.get(uri.to_string()))
205+
}
206+
}

testbench/src/lib.rs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod client;
2+
3+
pub use client::*;

0 commit comments

Comments
 (0)