-
Notifications
You must be signed in to change notification settings - Fork 10
rust testcontainer framework #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This is awesome, i'll give a look in the next days! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just left few initial comments, thanks for the contribution!
restate-test-utils/src/test_utils.rs
Outdated
} | ||
|
||
|
||
pub struct TestContainer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice if the test infra bootstraps the service endpoint server too, and then on Drop
stops the server.
restate-test-utils/src/test_utils.rs
Outdated
tokio::time::sleep(Duration::from_millis(milliseconds)).await; | ||
} | ||
|
||
pub async fn invoke(&self, service:Service, handler:String) -> Result<(), HandlerError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than this, i would start the user services http server and register them when bootstrapping the test infra, then just provide a url
here, and let people use the client they want to send test requests
@slinkydeveloper thanks for the feedback! Just pushed an update that captures the above. Here's the new test implementation: #[tokio::test]
async fn test_container_image() {
let mut test_container = TestContainer::new("docker.io/restatedev/restate", "latest").await.unwrap();
let endpoint = Endpoint::builder()
.bind(MyServiceImpl.serve())
.build();
test_container.serve_endpoint(endpoint).await;
// optionally insert a delays via tokio sleep
TestContainer::delay(1000).await;
// optionally call invoke on service handlers
use restate_sdk::service::Discoverable;
let my_service:Service = ServeMyService::<MyServiceImpl>::discover();
let invoke_response = test_container.invoke(my_service, "my_handler").await;
assert!(invoke_response.is_ok());
println!("invoke response:");
println!("{}", invoke_response.unwrap().text().await.unwrap());
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi! Thanks again for the contribution, I had some time to take a look, it's getting there, I think the API needs another pass
tests/test_container.rs
Outdated
@@ -0,0 +1,58 @@ | |||
use restate_test_utils::test_utils::TestContainer; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you move this test in restate-test-utils
? This way you don't need a dependency on restate-test-utils
in the root Cargo.toml. I'm afraid this might cause some issues
restate-test-utils/src/test_utils.rs
Outdated
.await? | ||
.json::<VersionResponse>() | ||
.await?; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a log line here (with tracing::info
) saying restate container started with the forwarded ports
restate-test-utils/src/test_utils.rs
Outdated
// todo cleanup on drop? | ||
// testcontainers-rs already implements stop/rm on drop] | ||
// https://docs.rs/testcontainers/latest/testcontainers/ | ||
// |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you probably need to stop the hyper server here on drop
restate-test-utils/src/test_utils.rs
Outdated
let mut container_stdout = container.stdout(true); | ||
// Spawn a task to copy data from the AsyncBufRead to stdout | ||
let stdout_logging = task::spawn(async move { | ||
let mut stdout = io::stdout(); | ||
if let Err(e) = io::copy(&mut container_stdout, &mut stdout).await { | ||
eprintln!("Error copying data: {}", e); | ||
} | ||
}); | ||
|
||
let mut container_stderr = container.stderr(true); | ||
// Spawn a task to copy data from the AsyncBufRead to stderr | ||
let stderr_logging = task::spawn(async move { | ||
let mut stderr = io::stderr(); | ||
if let Err(e) = io::copy(&mut container_stderr, &mut stderr).await { | ||
eprintln!("Error copying data: {}", e); | ||
} | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO this should be printed using the tracing::debug
macro. People can then configure a test tracing_subscriber
using https://docs.rs/tracing-test/latest/tracing_test/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a shot at doing this via tracing::debug and thought it was way too verbose. As an alternative I added a .with_container_logging()
option to the builder, but happy to drop back to tracing::debug if preferred.
tests/test_container.rs
Outdated
async fn test_container_image() { | ||
|
||
let mut test_container = TestContainer::new("docker.io/restatedev/restate", "latest").await.unwrap(); | ||
|
||
let endpoint = Endpoint::builder() | ||
.bind(MyServiceImpl.serve()) | ||
.build(); | ||
|
||
test_container.serve_endpoint(endpoint).await; | ||
|
||
// optionally insert a delays via tokio sleep | ||
TestContainer::delay(1000).await; | ||
|
||
// optionally call invoke on service handlers | ||
use restate_sdk::service::Discoverable; | ||
let my_service:Service = ServeMyService::<MyServiceImpl>::discover(); | ||
let invoke_response = test_container.invoke(my_service, "my_handler").await; | ||
|
||
assert!(invoke_response.is_ok()); | ||
|
||
println!("invoke response:"); | ||
println!("{}", invoke_response.unwrap().text().await.unwrap()); | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we're getting there with the API, maybe let me describe better what I had in mind that aligns with the other SDKs:
- For 90% of users, there should be a single API like
TestEnvironment::start(Endpoint)
that simply takes the endpoint, prepares the environment, registers the services etc. This API returns aTestEnvironment
with a methoduri()
that gives me the base address to send requests to restate. DroppingTestEnvironment
stops theEndpoint
too. People can then decide to send requests with whatever client they want, be it reqwest, or Implement ingress client #42. For example, something like that:
#[tokio::test]
async fn test_container_image() {
let endpoint = Endpoint::builder()
.bind(MyServiceImpl.serve())
.build();
let mut test_env = TestEnvironment::start(endpoint).await.unwrap();
let ingress_address: String = test_env.ingress_uri();
// Do whatever requests you want with reqwest, or some http test client crate like https://docs.rs/asserhttp/latest/asserhttp/#reqwest
// On Drop, TestEnvironment stops both container and Endpoint
}
- For the remaining 10% of the people, there should be a
TestEnvironment::builder().start()
that let's you tune few things (such as the image name, which by default should just be `"docker.io/restatedev/restate:latest".
restate-test-utils/src/test_utils.rs
Outdated
let host_port:u16 = 19080; | ||
let host_address = format!("0.0.0.0:{}", host_port); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need to start the TcpListener on port 0
, this way you get a random port assigned. Look at the implementation of HttpServer::listen_and_serve
/HttpServer::serve_with_cancel
restate-test-utils/src/test_utils.rs
Outdated
impl TestContainer { | ||
|
||
//"docker.io/restatedev/restate", "latest" | ||
pub async fn new(image:&str, version:&str) -> Result<Self, HandlerError> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should use anyhow::Error
here and everywhere else in this file, HandlerError
is an error type reserved to be used only inside restate service handlers.
restate-test-utils/Cargo.toml
Outdated
futures = "0.3.31" | ||
http = "1.2.0" | ||
nu-ansi-term = "0.50.1" | ||
reqwest = {version= "0.12.12", features = ["json"]} | ||
restate-sdk = { version = "0.3.2", path = ".." } | ||
restate-sdk-shared-core = "0.2.0" | ||
serde = "1.0.217" | ||
serde_json = "1.0.138" | ||
testcontainers = "0.23.1" | ||
tokio = "1.43.0" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also re-check which dependencies you need here? I think some of those are not needed
restate-test-utils/Cargo.toml
Outdated
edition = "2021" | ||
description = "Test Utilities for Restate SDK for Rust" | ||
license = "MIT" | ||
repository = "https://github.com/restatedev/sdk-rust/test-utils" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just https://github.com/restatedev/sdk-rust
is fine
restate-test-utils/Cargo.toml
Outdated
@@ -0,0 +1,21 @@ | |||
[package] | |||
name = "restate-test-utils" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe rename the package to restate-test-env
and the containing directory to test-env
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is awesome. Run just check
locally to fix linting/formatting issues and then we should be good to go!
max_admin_api_version:u32 | ||
} | ||
|
||
pub struct TestContainerBuilder { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
implement Default too calling new
test-env/src/lib.rs
Outdated
while let Err(_) = client.get(format!("{}/discover", self.endpoint_server_url.as_ref().unwrap())) | ||
.header("accept", "application/vnd.restate.endpointmanifest.v1+json") | ||
.send().await { | ||
tokio::time::sleep(Duration::from_secs(1)).await; | ||
|
||
warn!("retrying endpoint server"); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put some retry limit here? like after 10 times stop retrying
test-env/src/lib.rs
Outdated
client.get(format!("{}/discover", self.endpoint_server_url.as_ref().unwrap())) | ||
.header("accept", "application/vnd.restate.endpointmanifest.v1+json") | ||
.send().await?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this one too?
test-env/src/lib.rs
Outdated
while let Err(_) = client.get(format!("{}/discover", self.endpoint_server_url.as_ref().unwrap())) | ||
.header("accept", "application/vnd.restate.endpointmanifest.v1+json") | ||
.send().await { | ||
tokio::time::sleep(Duration::from_secs(1)).await; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can lower this down to 100 msecs, and retry for at most 5 seconds.
test-env/tests/test_container.rs
Outdated
let ingress_url = test_container.ingress_url(); | ||
|
||
// call container ingress url for /MyService/my_handler | ||
let resposne = reqwest::Client::new().post(format!("{}/MyService/my_handler", ingress_url)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo
thanks again for the recs and sorry for not running lint/fmt! should be good to go now |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One minor nit, and I merge
test-env/Cargo.toml
Outdated
@@ -0,0 +1,21 @@ | |||
[package] | |||
name = "test-env" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this name is the crate name on crates.io, so it needs to be restate-sdk-test-env
(unlike the directory name, which test-env
is just fine)
README.md
Outdated
### Testing | ||
|
||
The SDK uses [Testcontainers](https://rust.testcontainers.org/) to support integration testing using a Docker-deployed restate server. | ||
The `test-env` crate provides a framework for initializing the test environment, and an integration test example in `test-env/tests/test_container.rs`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The `test-env` crate provides a framework for initializing the test environment, and an integration test example in `test-env/tests/test_container.rs`. | |
The `restate-sdk-test-env` crate provides a framework for initializing the test environment, and an integration test example in `test-env/tests/test_container.rs`. |
Here's my initial shot at a testcontainers implementation for the Rust SDK.
I've got the core functionality working, but wanted to share while in progress to see if adding a new "restate-test-utils" crate to the SDK is ok. Also wanted to solicit feedback on ways to extend the test implementation (e.g. mirroring Temporal's approach), per discussions on Discord.
Here's the current test implementation for a service as defined in
tests/test_container.rs