diff --git a/Cargo.toml b/Cargo.toml index cc9e4596..c88be5dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,14 +17,15 @@ edition = "2018" members = [ "actix-codec", "actix-connect", + "actix-ioframe", "actix-rt", "actix-service", "actix-server", "actix-server-config", + "actix-testing", "actix-test-server", "actix-threadpool", "actix-tower", - "actix-ioframe", "actix-utils", "router", ] diff --git a/actix-testing/Cargo.toml b/actix-testing/Cargo.toml new file mode 100644 index 00000000..a6083f07 --- /dev/null +++ b/actix-testing/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "actix-testing" +version = "0.1.0" +authors = ["Nikolay Kim "] +description = "Actix test server" +keywords = ["network", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-net.git" +documentation = "https://docs.rs/actix-testing/" +categories = ["network-programming", "asynchronous"] +license = "MIT/Apache-2.0" +exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"] +edition = "2018" +workspace = ".." + +[package.metadata.docs.rs] +features = ["ssl", "tls", "rust-tls"] + +[lib] +name = "actix_testing" +path = "src/lib.rs" + +[features] +default = [] + +# openssl +ssl = ["openssl", "actix-server/ssl"] + +# rustls +rust-tls = ["rustls", "tokio-rustls", "webpki", "webpki-roots"] + +[dependencies] +actix-rt = "0.2.1" +actix-server = "0.6.0" +actix-server-config = "0.1.0" +actix-service = "0.4.0" + +log = "0.4" +net2 = "0.2" +futures = "0.1" +tokio-tcp = "0.1" +tokio-reactor = "0.1" + +# openssl +openssl = { version="0.10", optional = true } + +#rustls +rustls = { version = "^0.15", optional = true } +tokio-rustls = { version = "^0.9", optional = true } +webpki = { version = "0.19", optional = true } +webpki-roots = { version = "0.16", optional = true } diff --git a/actix-testing/LICENSE-APACHE b/actix-testing/LICENSE-APACHE new file mode 120000 index 00000000..965b606f --- /dev/null +++ b/actix-testing/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-testing/LICENSE-MIT b/actix-testing/LICENSE-MIT new file mode 120000 index 00000000..76219eb7 --- /dev/null +++ b/actix-testing/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-testing/src/lib.rs b/actix-testing/src/lib.rs new file mode 100644 index 00000000..f3607f92 --- /dev/null +++ b/actix-testing/src/lib.rs @@ -0,0 +1,152 @@ +//! Various helpers for Actix applications to use during testing. +use std::sync::mpsc; +use std::{net, thread}; + +use actix_rt::System; +use actix_server::{Server, ServerBuilder, StreamServiceFactory}; +pub use actix_server_config::{Io, ServerConfig}; + +use net2::TcpBuilder; +use tokio_reactor::Handle; +use tokio_tcp::TcpStream; + +mod rt; +pub use self::rt::*; + +/// The `TestServer` type. +/// +/// `TestServer` is very simple test server that simplify process of writing +/// integration tests for actix-net applications. +/// +/// # Examples +/// +/// ```rust +/// use actix_service::{service_fn, IntoNewService}; +/// use actix_testing::TestServer; +/// +/// fn main() { +/// let srv = TestServer::with(|| service_fn( +/// |sock| { +/// println!("New connection: {:?}", sock); +/// Ok::<_, ()>(()) +/// } +/// )); +/// +/// println!("SOCKET: {:?}", srv.connect()); +/// } +/// ``` +pub struct TestServer; + +/// Test server runstime +pub struct TestServerRuntime { + addr: net::SocketAddr, + host: String, + port: u16, + system: System, +} + +impl TestServer { + /// Start new server with server builder + pub fn new(mut factory: F) -> TestServerRuntime + where + F: FnMut(ServerBuilder) -> ServerBuilder + Send + 'static, + { + let (tx, rx) = mpsc::channel(); + + // run server in separate thread + thread::spawn(move || { + let sys = System::new("actix-test-server"); + factory(Server::build()) + .workers(1) + .disable_signals() + .start(); + + tx.send(System::current()).unwrap(); + sys.run() + }); + let system = rx.recv().unwrap(); + + TestServerRuntime { + system, + addr: "127.0.0.1:0".parse().unwrap(), + host: "127.0.0.1".to_string(), + port: 0, + } + } + + /// Start new test server with application factory + pub fn with>(factory: F) -> TestServerRuntime { + let (tx, rx) = mpsc::channel(); + + // run server in separate thread + thread::spawn(move || { + let sys = System::new("actix-test-server"); + let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); + let local_addr = tcp.local_addr().unwrap(); + + Server::build() + .listen("test", tcp, factory)? + .workers(1) + .disable_signals() + .start(); + + tx.send((System::current(), local_addr)).unwrap(); + sys.run() + }); + + let (system, addr) = rx.recv().unwrap(); + + let host = format!("{}", addr.ip()); + let port = addr.port(); + + TestServerRuntime { + system, + addr, + host, + port, + } + } + + /// Get firat available unused local address + pub fn unused_addr() -> net::SocketAddr { + let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); + let socket = TcpBuilder::new_v4().unwrap(); + socket.bind(&addr).unwrap(); + socket.reuse_address(true).unwrap(); + let tcp = socket.to_tcp_listener().unwrap(); + tcp.local_addr().unwrap() + } +} + +impl TestServerRuntime { + /// Test server host + pub fn host(&self) -> &str { + &self.host + } + + /// Test server port + pub fn port(&self) -> u16 { + self.port + } + + /// Get test server address + pub fn addr(&self) -> net::SocketAddr { + self.addr + } + + /// Stop http server + fn stop(&mut self) { + self.system.stop(); + } + + /// Connect to server, return tokio TcpStream + pub fn connect(&self) -> std::io::Result { + TcpStream::from_std(net::TcpStream::connect(self.addr)?, &Handle::default()) + } +} + +impl Drop for TestServerRuntime { + fn drop(&mut self) { + self.stop() + } +} diff --git a/actix-testing/src/rt.rs b/actix-testing/src/rt.rs new file mode 100644 index 00000000..488f3d65 --- /dev/null +++ b/actix-testing/src/rt.rs @@ -0,0 +1,116 @@ +//! Various helpers for Actix applications to use during testing. +use std::cell::RefCell; + +use actix_rt::{System, SystemRunner}; +use actix_service::Service; +use futures::future::{lazy, Future, IntoFuture}; + +thread_local! { + static RT: RefCell = { + RefCell::new(Inner(Some(System::builder().build()))) + }; +} + +struct Inner(Option); + +impl Inner { + fn get_mut(&mut self) -> &mut SystemRunner { + self.0.as_mut().unwrap() + } +} + +impl Drop for Inner { + fn drop(&mut self) { + std::mem::forget(self.0.take().unwrap()) + } +} + +/// Runs the provided future, blocking the current thread until the future +/// completes. +/// +/// This function can be used to synchronously block the current thread +/// until the provided `future` has resolved either successfully or with an +/// error. The result of the future is then returned from this function +/// call. +/// +/// Note that this function is intended to be used only for testing purpose. +/// This function panics on nested call. +pub fn block_on(f: F) -> Result +where + F: IntoFuture, +{ + RT.with(move |rt| rt.borrow_mut().get_mut().block_on(f.into_future())) +} + +/// Runs the provided function, blocking the current thread until the result +/// future completes. +/// +/// This function can be used to synchronously block the current thread +/// until the provided `future` has resolved either successfully or with an +/// error. The result of the future is then returned from this function +/// call. +/// +/// Note that this function is intended to be used only for testing purpose. +/// This function panics on nested call. +pub fn block_fn(f: F) -> Result +where + F: FnOnce() -> R, + R: IntoFuture, +{ + RT.with(move |rt| rt.borrow_mut().get_mut().block_on(lazy(f))) +} + +/// Spawn future to the current test runtime. +pub fn spawn(fut: F) +where + F: Future + 'static, +{ + run_on(move || { + actix_rt::spawn(fut); + }); +} + +/// Runs the provided function, with runtime enabled. +/// +/// Note that this function is intended to be used only for testing purpose. +/// This function panics on nested call. +pub fn run_on(f: F) -> R +where + F: FnOnce() -> R, +{ + RT.with(move |rt| { + rt.borrow_mut() + .get_mut() + .block_on(lazy(|| Ok::<_, ()>(f()))) + }) + .unwrap() +} + +/// Calls service and waits for response future completion. +/// +/// ```rust,ignore +/// use actix_web::{test, App, HttpResponse, http::StatusCode}; +/// use actix_service::Service; +/// +/// #[test] +/// fn test_response() { +/// let mut app = test::init_service( +/// App::new() +/// .service(web::resource("/test").to(|| HttpResponse::Ok())) +/// ); +/// +/// // Create request object +/// let req = test::TestRequest::with_uri("/test").to_request(); +/// +/// // Call application +/// let resp = test::call_service(&mut app, req); +/// assert_eq!(resp.status(), StatusCode::OK); +/// } +/// ``` +pub fn call_service(app: &mut S, req: R) -> S::Response +where + S: Service, + S::Error: std::fmt::Debug, +{ + block_on(run_on(move || app.call(req))).unwrap() +}