Using a background warp server in test

I'm starting a warp server in a test, and try get files from it with a Command::new("/usr/bin/curl").
Everything seems to work fine except that the test never terminates....

I'm setting up the Warp server in a function

async fn start_web_server() {
    let data = warp::get()
        .and(warp::path("data"))
        .and(warp::fs::dir("./tests/data"));
    let routes = data;
    println!("will start serve");
    warp::serve(routes).run(([127, 0, 0, 1], 3030)).await
}

Then spawn it

    let tr = tokio::runtime::Runtime::new().unwrap();
    let join_handle = tr.spawn(async {
        println!("spawning");
        let server = start_web_server();
        server.await
    });

I then assert the curl call:

    let mut cmd = Command::new("/usr/bin/curl");
    cmd.arg("http://localhost:3030/data/valid/the_file.txt");
    cmd.assert().success();
    let r = join_handle.await;

This seems to work, however, if I then call

    let r = join_handle.await;

the test doesn't finish. I can curl the file from another therminal though, so the warp server is up.
And if I don't await on join_handle, I get the error

Cannot drop a runtime in a context where blocking is not allowed.
This happens when a runtime is dropped from within an asynchronous context.

I understand what it says, but I don't know how I could fix it to get the behaviour I need.
What's the way to get the test running and reporting its result?

I'm a rust beginner, so any additional pointers are welcome :wink:

Thanks in advance!

You may need to tell warp to shutdown when done.

Also, are you creating a tokio runtime manually? In a test? Try using #[tokio::test] instead of #[test], and then your test function should be an async fn and you don't have to manage the runtime explicitly.

2 Likes

There is also the warp::test module that provides utilities for testing your filters. Might be easier than spawning a curl process.

1 Like

Thanks for the reactions!

@kaj : I'm using #[tokio::test]] (see below), but I am creating the runtime manually to be able to spawn. I actually have a version with bind_with_graceful_shutdown (here), but then the client (also a curl in another terminal) hangs (waits) with no download taking place.

@jofas : Actually I'm testing another command line utility, but I used curl for the illustration, to avoid questions about the binary itself :smiley:

The whole code is:

use assert_cmd::prelude::*; // Add methods on commands
use std::process::Command;
use tokio::time::{sleep, Duration};
use warp::Filter;

async fn start_web_server() {
    let data = warp::get()
        .and(warp::path("data"))
        .and(warp::fs::dir("./tests/data"));

    let routes = data;

    println!("will start serve");
    warp::serve(routes).run(([127, 0, 0, 1], 3030)).await
}

#[tokio::test]
async fn it_works() -> Result<(), Box<dyn std::error::Error>> {
    let tr = tokio::runtime::Runtime::new().unwrap();
    let join_handle = tr.spawn(async {
        println!("spawning");
        let server = start_web_server();
        server.await
    });

    let mut cmd = Command::new("/usr/bin/curl");
    cmd.arg("http://localhost:3030/data/valid/the_file.txt");
    cmd.assert().success();
    let r = join_handle.await;
    println!("result : ${:?}", r);
    Ok(())
}

In an async function, you can do tokio::task::spawn so you don't have to start another runtime (when you are in an async fn, there already is a runtime).

3 Likes

So, I think I duplicated the example for bind_with_graceful_shutdown, but warp is not serving files.

Using tcpdump and curl -v I see the connection is opened and the request is sent, but no response is coming back. I'm currently stuck, and if someone has a suggestion it is very much welcome :slight_smile:

The full code and its output:

async fn start_web_server() -> tokio::sync::oneshot::Sender<()> {
    let (tx, rx) = oneshot::channel::<()>();
    let data = warp::get()
        .and(warp::path("data"))
        .and(warp::fs::dir("./tests/data"));
    let routes = data;

    let (addr, server) =
        warp::serve(routes).bind_with_graceful_shutdown(([127, 0, 0, 1], 3030), async {
            println!("** Will wait on rx");
            rx.await.ok();
        });
    println!("will spawn");
    tokio::task::spawn(server);
    println!("Address : {:?}", addr);
    tx
}

#[tokio::test]
async fn it_works() -> Result<(), Box<dyn std::error::Error>> {
    println!("will start server");
    let tx = start_web_server().await;
    println!("started server");
    sleep(Duration::from_millis(1000)).await;

    let mut cmd = Command::cargo_bin("asfd")?;
    cmd.arg("http://localhost:3030/data/valid/the_file.txt");
    println!("will assert success");
    cmd.assert().success();
    println!("asserted success");
    println!("will send");
    let _ = tx.send(());
    Ok(())
}

and the output of a run is:

running 1 test
will start server
will spawn
Address : 127.0.0.1:3030
started server
** Will wait on rx
will assert success

Ah, I guess the std non-async Command blocks the entire runtime. Try using a tokio::process::Command instead.

What I believe happens here, is that warp is listening for requests, but can't actually handle anything until the command is done (but when the command is done, warp is immediately told to shut down). In general, when using any async code, you want to avoid all potentially blocking synchronous blocking code.

2 Likes

Thanks, that was it indeed.
For the reference, below is a working version.

I was using facilities (cargo_bin) provided by assert_cmd which I'm not sure can be used with tokio::process::Command. So I'll need to rework this part, but the code below works fine calling curl.

use warp::Filter;

use tokio::process::Command;
use tokio::sync::oneshot;

async fn start_web_server() -> tokio::sync::oneshot::Sender<()> {
    let (tx, rx) = oneshot::channel::<()>();
    let data = warp::get()
        .and(warp::path("data"))
        .and(warp::fs::dir("./tests/data"));

    let routes = data;

    let (addr, server) =
        warp::serve(routes).bind_with_graceful_shutdown(([127, 0, 0, 1], 3030), async {
            println!("** Will wait on rx");
            rx.await.ok();
            println!("** stopping");
        });
    println!("will spawn");
    tokio::task::spawn(server);
    println!("Address : {:?}", addr);
    tx
}

#[tokio::test]
async fn it_works() -> Result<(), Box<dyn std::error::Error>> {
    println!("will start server");
    let tx = start_web_server();
    let v = tokio::task::spawn(tx);
    println!("started server");

    let mut cmd = Command::new("/usr/bin/curl");
    cmd.arg("-v");
    cmd.arg("http://localhost:3030/data/valid/the_file.txt");

    let mut child = cmd.spawn().unwrap();
    let status = child.wait().await?;
    println!("the command exited with: {}", status);

    println!("will send");
    let _ = v.await?.send(());
    Ok(())
}