From 4e1690d8cc5da7986513014d7bbd382303e741a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oddbj=C3=B8rn=20Gr=C3=B8dem?= <29732646+oddgrd@users.noreply.github.com> Date: Thu, 9 Mar 2023 13:34:16 +0000 Subject: [PATCH] Feature: eng 483 trim and fix the tests in shuttle-service (#693) * tests: update build_crate tests * tests: remove loader tests * feat: cleanup service deps * feat: setup integration tests in runtime * feat: expected panic message for not_shuttle * refactor: simplify dummy provisioner * feat: re-export main and service from runtime --- Cargo.lock | 6 +- runtime/Cargo.toml | 5 + runtime/src/bin/rocket.rs | 2 +- runtime/src/legacy/mod.rs | 2 +- runtime/src/lib.rs | 1 + runtime/src/next/mod.rs | 2 +- runtime/tests/integration/helpers.rs | 95 +++++++++ runtime/tests/integration/loader.rs | 48 +++++ runtime/tests/integration/main.rs | 2 + .../tests/resources/bind-panic/Cargo.toml | 5 +- .../tests/resources/bind-panic/src/main.rs | 13 ++ service/Cargo.toml | 5 - service/src/builder.rs | 43 +++- service/tests/integration/build_crate.rs | 61 ++---- service/tests/integration/helpers/loader.rs | 28 --- service/tests/integration/helpers/mod.rs | 4 - service/tests/integration/helpers/sqlx.rs | 134 ------------ service/tests/integration/loader.rs | 192 ------------------ service/tests/integration/main.rs | 7 +- service/tests/resources/bind-panic/src/lib.rs | 18 -- .../tests/resources/build-panic/Cargo.toml | 12 -- .../tests/resources/build-panic/src/lib.rs | 18 -- service/tests/resources/is-bin/Cargo.toml | 12 ++ service/tests/resources/is-bin/src/main.rs | 6 + service/tests/resources/is-cdylib/src/lib.rs | 5 - .../{is-cdylib => not-bin}/Cargo.toml | 3 +- .../{not-cdylib => not-bin}/src/lib.rs | 2 + service/tests/resources/not-cdylib/Cargo.toml | 13 -- service/tests/resources/not-lib/Cargo.toml | 6 - service/tests/resources/not-lib/src/main.rs | 5 - .../tests/resources/not-shuttle/Cargo.toml | 8 +- .../tests/resources/not-shuttle/src/lib.rs | 1 - .../tests/resources/not-shuttle/src/main.rs | 6 + .../tests/resources/sleep-async/Cargo.toml | 13 -- .../tests/resources/sleep-async/src/lib.rs | 26 --- service/tests/resources/sleep/Cargo.toml | 12 -- service/tests/resources/sleep/src/lib.rs | 25 --- service/tests/resources/sqlx-pool/Cargo.toml | 14 -- service/tests/resources/sqlx-pool/src/lib.rs | 39 ---- 39 files changed, 261 insertions(+), 638 deletions(-) create mode 100644 runtime/tests/integration/helpers.rs create mode 100644 runtime/tests/integration/loader.rs create mode 100644 runtime/tests/integration/main.rs rename {service => runtime}/tests/resources/bind-panic/Cargo.toml (56%) create mode 100644 runtime/tests/resources/bind-panic/src/main.rs delete mode 100644 service/tests/integration/helpers/loader.rs delete mode 100644 service/tests/integration/helpers/mod.rs delete mode 100644 service/tests/integration/helpers/sqlx.rs delete mode 100644 service/tests/integration/loader.rs delete mode 100644 service/tests/resources/bind-panic/src/lib.rs delete mode 100644 service/tests/resources/build-panic/Cargo.toml delete mode 100644 service/tests/resources/build-panic/src/lib.rs create mode 100644 service/tests/resources/is-bin/Cargo.toml create mode 100644 service/tests/resources/is-bin/src/main.rs delete mode 100644 service/tests/resources/is-cdylib/src/lib.rs rename service/tests/resources/{is-cdylib => not-bin}/Cargo.toml (75%) rename service/tests/resources/{not-cdylib => not-bin}/src/lib.rs (71%) delete mode 100644 service/tests/resources/not-cdylib/Cargo.toml delete mode 100644 service/tests/resources/not-lib/Cargo.toml delete mode 100644 service/tests/resources/not-lib/src/main.rs delete mode 100644 service/tests/resources/not-shuttle/src/lib.rs create mode 100644 service/tests/resources/not-shuttle/src/main.rs delete mode 100644 service/tests/resources/sleep-async/Cargo.toml delete mode 100644 service/tests/resources/sleep-async/src/lib.rs delete mode 100644 service/tests/resources/sleep/Cargo.toml delete mode 100644 service/tests/resources/sleep/src/lib.rs delete mode 100644 service/tests/resources/sqlx-pool/Cargo.toml delete mode 100644 service/tests/resources/sqlx-pool/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index c47655c27..5bfd7393d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6403,8 +6403,10 @@ dependencies = [ "cap-std", "chrono", "clap 4.0.27", + "crossbeam-channel", "futures", "hyper", + "portpicker", "rmp-serde", "rocket", "serde_json", @@ -6445,20 +6447,17 @@ dependencies = [ "cargo", "cargo_metadata", "crossbeam-channel", - "futures", "hyper", "num_cpus", "pipe", "poem", "poise", - "portpicker", "rocket", "salvo", "serde_json", "serenity", "shuttle-codegen", "shuttle-common", - "sqlx", "thiserror", "thruster", "tide", @@ -6466,7 +6465,6 @@ dependencies = [ "tower", "tracing", "tracing-subscriber", - "uuid 1.2.2", "warp", ] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b8ce5c235..36f83ee99 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -52,6 +52,11 @@ workspace = true workspace = true features = ["builder", "web-rocket"] # TODO: remove web-rocket +[dev-dependencies] +crossbeam-channel = "0.5.6" +portpicker = "0.1.1" +futures = { version = "0.3.25" } + [features] next = [ "cap-std", diff --git a/runtime/src/bin/rocket.rs b/runtime/src/bin/rocket.rs index 9222f709d..db3836826 100644 --- a/runtime/src/bin/rocket.rs +++ b/runtime/src/bin/rocket.rs @@ -6,7 +6,7 @@ async fn main() { async fn loader( mut factory: shuttle_runtime::ProvisionerFactory, - logger: shuttle_runtime::Logger, + _logger: shuttle_runtime::Logger, ) -> shuttle_service::ShuttleRocket { use shuttle_service::ResourceBuilder; diff --git a/runtime/src/legacy/mod.rs b/runtime/src/legacy/mod.rs index 9cc3fa4b3..cad51259c 100644 --- a/runtime/src/legacy/mod.rs +++ b/runtime/src/legacy/mod.rs @@ -231,7 +231,7 @@ where let kill_tx = self.kill_tx.lock().unwrap().deref_mut().take(); if let Some(kill_tx) = kill_tx { - if kill_tx.send(format!("stopping deployment")).is_err() { + if kill_tx.send("stopping deployment".to_owned()).is_err() { error!("the receiver dropped"); return Err(Status::internal("failed to stop deployment")); } diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 6e51ff0f4..233616493 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -10,3 +10,4 @@ pub use logger::Logger; pub use next::{AxumWasm, NextArgs}; pub use provisioner_factory::ProvisionerFactory; pub use shuttle_common::storage_manager::StorageManager; +pub use shuttle_service::{main, Error, Service}; diff --git a/runtime/src/next/mod.rs b/runtime/src/next/mod.rs index ccd30e321..41f50eaac 100644 --- a/runtime/src/next/mod.rs +++ b/runtime/src/next/mod.rs @@ -155,7 +155,7 @@ impl Runtime for AxumWasm { let kill_tx = self.kill_tx.lock().unwrap().deref_mut().take(); if let Some(kill_tx) = kill_tx { - if kill_tx.send(format!("stopping deployment")).is_err() { + if kill_tx.send("stopping deployment".to_owned()).is_err() { error!("the receiver dropped"); return Err(Status::internal("failed to stop deployment")); } diff --git a/runtime/tests/integration/helpers.rs b/runtime/tests/integration/helpers.rs new file mode 100644 index 000000000..afa093f82 --- /dev/null +++ b/runtime/tests/integration/helpers.rs @@ -0,0 +1,95 @@ +use std::{ + collections::HashMap, + net::{Ipv4Addr, SocketAddr}, + path::{Path, PathBuf}, +}; + +use anyhow::Result; +use async_trait::async_trait; +use shuttle_proto::{ + provisioner::{ + provisioner_server::{Provisioner, ProvisionerServer}, + DatabaseRequest, DatabaseResponse, + }, + runtime::{self, runtime_client::RuntimeClient}, +}; +use shuttle_service::builder::{build_crate, Runtime}; +use tonic::{ + transport::{Channel, Server}, + Request, Response, Status, +}; + +pub struct TestRuntime { + pub runtime_client: RuntimeClient, + pub bin_path: String, + pub service_name: String, + pub runtime_address: SocketAddr, + pub secrets: HashMap, +} + +pub async fn spawn_runtime(project_path: String, service_name: &str) -> Result { + let provisioner_address = SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + portpicker::pick_unused_port().unwrap(), + ); + let runtime_port = portpicker::pick_unused_port().unwrap(); + let runtime_address = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), runtime_port); + + let (tx, _) = crossbeam_channel::unbounded(); + let runtime = build_crate(Path::new(&project_path), false, tx).await?; + + let secrets: HashMap = Default::default(); + + let (is_wasm, bin_path) = match runtime { + Runtime::Next(path) => (true, path), + Runtime::Legacy(path) => (false, path), + }; + + start_provisioner(DummyProvisioner, provisioner_address); + + // TODO: update this to work with shuttle-next projects, see cargo-shuttle local run + let runtime_path = || bin_path.clone(); + + let (_, runtime_client) = runtime::start( + is_wasm, + runtime::StorageManagerType::WorkingDir(PathBuf::from(project_path.clone())), + &format!("http://{}", provisioner_address), + runtime_port, + runtime_path, + ) + .await?; + + Ok(TestRuntime { + runtime_client, + bin_path: bin_path + .into_os_string() + .into_string() + .expect("to convert path to string"), + service_name: service_name.to_string(), + runtime_address, + secrets, + }) +} + +/// A dummy provisioner for tests, a provisioner connection is required +/// to start a project runtime. +pub struct DummyProvisioner; + +fn start_provisioner(provisioner: DummyProvisioner, address: SocketAddr) { + tokio::spawn(async move { + Server::builder() + .add_service(ProvisionerServer::new(provisioner)) + .serve(address) + .await + }); +} + +#[async_trait] +impl Provisioner for DummyProvisioner { + async fn provision_database( + &self, + _request: Request, + ) -> Result, Status> { + panic!("did not expect any runtime test to use dbs") + } +} diff --git a/runtime/tests/integration/loader.rs b/runtime/tests/integration/loader.rs new file mode 100644 index 000000000..d4051818a --- /dev/null +++ b/runtime/tests/integration/loader.rs @@ -0,0 +1,48 @@ +use std::time::Duration; + +use shuttle_proto::runtime::{LoadRequest, StartRequest}; +use uuid::Uuid; + +use crate::helpers::{spawn_runtime, TestRuntime}; + +/// This test does panic, but the panic happens in a spawned task inside the project runtime, +/// so we get this output: `thread 'tokio-runtime-worker' panicked at 'panic in bind', src/main.rs:6:9`, +/// but `should_panic(expected = "panic in bind")` doesn't catch it. +#[tokio::test] +#[should_panic(expected = "panic in bind")] +async fn bind_panic() { + let project_path = format!("{}/tests/resources/bind-panic", env!("CARGO_MANIFEST_DIR")); + + let TestRuntime { + bin_path, + service_name, + secrets, + mut runtime_client, + runtime_address, + } = spawn_runtime(project_path, "bind-panic").await.unwrap(); + + let load_request = tonic::Request::new(LoadRequest { + path: bin_path, + service_name, + secrets, + }); + + let _ = runtime_client.load(load_request).await.unwrap(); + + let start_request = StartRequest { + deployment_id: Uuid::default().as_bytes().to_vec(), + ip: runtime_address.to_string(), + }; + + // I also tried this without spawning, but it gave the same result. Panic but it isn't caught. + tokio::spawn(async move { + runtime_client + .start(tonic::Request::new(start_request)) + .await + .unwrap(); + // Give it a second to panic. + tokio::time::sleep(Duration::from_secs(1)).await; + }) + .await + .unwrap(); +} diff --git a/runtime/tests/integration/main.rs b/runtime/tests/integration/main.rs new file mode 100644 index 000000000..e52d9c483 --- /dev/null +++ b/runtime/tests/integration/main.rs @@ -0,0 +1,2 @@ +pub mod helpers; +pub mod loader; diff --git a/service/tests/resources/bind-panic/Cargo.toml b/runtime/tests/resources/bind-panic/Cargo.toml similarity index 56% rename from service/tests/resources/bind-panic/Cargo.toml rename to runtime/tests/resources/bind-panic/Cargo.toml index eb3331848..05c792584 100644 --- a/service/tests/resources/bind-panic/Cargo.toml +++ b/runtime/tests/resources/bind-panic/Cargo.toml @@ -3,10 +3,9 @@ name = "bind-panic" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] [workspace] [dependencies] -shuttle-service = { path = "../../../" } +shuttle-runtime = { path = "../../../" } +tokio = { version = "1.22.0" } diff --git a/runtime/tests/resources/bind-panic/src/main.rs b/runtime/tests/resources/bind-panic/src/main.rs new file mode 100644 index 000000000..d7badcd69 --- /dev/null +++ b/runtime/tests/resources/bind-panic/src/main.rs @@ -0,0 +1,13 @@ +struct MyService; + +#[shuttle_runtime::async_trait] +impl shuttle_runtime::Service for MyService { + async fn bind(mut self, _: std::net::SocketAddr) -> Result<(), shuttle_runtime::Error> { + panic!("panic in bind"); + } +} + +#[shuttle_runtime::main] +async fn bind_panic() -> Result { + Ok(MyService) +} diff --git a/service/Cargo.toml b/service/Cargo.toml index f9943a4d2..ceb48a7ef 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -19,7 +19,6 @@ bincode = { version = "1.3.3", optional = true } cargo = { version = "0.65.0", optional = true } cargo_metadata = "0.15.2" crossbeam-channel = "0.5.6" -futures = { version = "0.3.25", features = ["std"] } hyper = { version = "0.14.23", features = ["server", "tcp", "http1"], optional = true } num_cpus = { version = "1.14.0", optional = true } pipe = "0.4.0" @@ -36,7 +35,6 @@ tokio = { version = "1.22.0", features = ["sync"] } tower = { version = "0.4.13", features = ["make"], optional = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } -uuid = { workspace = true, features = ["v4"] } warp = { version = "0.3.3", optional = true } # Tide does not have tokio support. So make sure async-std is compatible with tokio @@ -55,10 +53,7 @@ workspace = true features = ["tracing", "service"] [dev-dependencies] -portpicker = "0.1.1" -sqlx = { version = "0.6.2", features = ["runtime-tokio-native-tls", "postgres"] } tokio = { version = "1.22.0", features = ["macros", "rt"] } -uuid = { workspace = true, features = ["v4"] } [features] default = ["codegen"] diff --git a/service/src/builder.rs b/service/src/builder.rs index 3fcd8c67f..1d1358a8b 100644 --- a/service/src/builder.rs +++ b/service/src/builder.rs @@ -1,8 +1,8 @@ use std::path::{Path, PathBuf}; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, bail, Context}; use cargo::core::compiler::{CompileKind, CompileMode, CompileTarget, MessageFormat}; -use cargo::core::{Shell, Summary, Verbosity, Workspace}; +use cargo::core::{Manifest, Shell, Summary, Verbosity, Workspace}; use cargo::ops::{clean, compile, CleanOptions, CompileOptions}; use cargo::util::interning::InternedString; use cargo::util::{homedir, ToSemver}; @@ -51,14 +51,18 @@ pub async fn build_crate( let manifest_path = project_path.join("Cargo.toml"); let mut ws = Workspace::new(&manifest_path, &config)?; - let current = ws.current_mut().map_err(|_| anyhow!("A Shuttle project cannot have a virtual manifest file - please ensure your Cargo.toml file specifies it as a library."))?; + let current = ws.current_mut().map_err(|_| anyhow!("A Shuttle project cannot have a virtual manifest file - please ensure the `package` table is present in your Cargo.toml file."))?; let summary = current.manifest_mut().summary_mut(); - let is_next = is_next(summary); + if !is_next { check_version(summary)?; + ensure_binary(current.manifest())?; + } else { + ensure_cdylib(current.manifest_mut())?; } + check_no_panic(&ws)?; let opts = get_compile_options(&config, release_mode, is_next)?; @@ -173,6 +177,37 @@ fn is_next(summary: &Summary) -> bool { .any(|dependency| dependency.package_name() == NEXT_NAME) } +/// Make sure the project is a binary for legacy projects. +fn ensure_binary(manifest: &Manifest) -> anyhow::Result<()> { + if manifest.targets().iter().any(|target| target.is_bin()) { + Ok(()) + } else { + bail!("Your Shuttle project must be a binary.") + } +} + +/// Make sure "cdylib" is set for shuttle-next projects, else set it if possible. +fn ensure_cdylib(manifest: &mut Manifest) -> anyhow::Result<()> { + if let Some(target) = manifest + .targets_mut() + .iter_mut() + .find(|target| target.is_lib()) + { + if !target.is_cdylib() { + *target = cargo::core::manifest::Target::lib_target( + target.name(), + vec![cargo::core::compiler::CrateType::Cdylib], + target.src_path().path().unwrap().to_path_buf(), + target.edition(), + ); + } + + Ok(()) + } else { + bail!("Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file.") + } +} + /// Check that the crate being build is compatible with this version of loader fn check_version(summary: &Summary) -> anyhow::Result<()> { let valid_version = VERSION.to_semver().unwrap(); diff --git a/service/tests/integration/build_crate.rs b/service/tests/integration/build_crate.rs index 6c7e289eb..f36a46d5a 100644 --- a/service/tests/integration/build_crate.rs +++ b/service/tests/integration/build_crate.rs @@ -1,64 +1,39 @@ use std::path::{Path, PathBuf}; -use shuttle_service::loader::{build_crate, Runtime}; +use shuttle_service::builder::{build_crate, Runtime}; -#[tokio::test(flavor = "multi_thread")] +#[tokio::test] +#[should_panic(expected = "1 job failed")] async fn not_shuttle() { let (tx, _) = crossbeam_channel::unbounded(); let project_path = format!("{}/tests/resources/not-shuttle", env!("CARGO_MANIFEST_DIR")); - let so_path = match build_crate(Default::default(), Path::new(&project_path), false, tx) + build_crate(Path::new(&project_path), false, tx) .await - .unwrap() - { - Runtime::Legacy(path) => path, - _ => unreachable!(), - }; - - assert!( - so_path - .display() - .to_string() - .ends_with("tests/resources/not-shuttle/target/debug/libnot_shuttle.so"), - "did not get expected so_path: {}", - so_path.display() - ); + .unwrap(); } #[tokio::test] -#[should_panic( - expected = "Your Shuttle project must be a library. Please add `[lib]` to your Cargo.toml file." -)] -async fn not_lib() { +#[should_panic(expected = "Your Shuttle project must be a binary.")] +async fn not_bin() { let (tx, _) = crossbeam_channel::unbounded(); - let project_path = format!("{}/tests/resources/not-lib", env!("CARGO_MANIFEST_DIR")); - build_crate(Default::default(), Path::new(&project_path), false, tx) - .await - .unwrap(); + let project_path = format!("{}/tests/resources/not-bin", env!("CARGO_MANIFEST_DIR")); + match build_crate(Path::new(&project_path), false, tx).await { + Ok(_) => {} + Err(e) => panic!("{}", e.to_string()), + } } -#[tokio::test(flavor = "multi_thread")] -async fn not_cdylib() { +#[tokio::test] +async fn is_bin() { let (tx, _) = crossbeam_channel::unbounded(); - let project_path = format!("{}/tests/resources/not-cdylib", env!("CARGO_MANIFEST_DIR")); - assert!(matches!( - build_crate(Default::default(), Path::new(&project_path), false, tx).await, - Ok(Runtime::Legacy(_)) - )); - assert!(PathBuf::from(project_path) - .join("target/debug/libnot_cdylib.so") - .exists()); -} + let project_path = format!("{}/tests/resources/is-bin", env!("CARGO_MANIFEST_DIR")); -#[tokio::test(flavor = "multi_thread")] -async fn is_cdylib() { - let (tx, _) = crossbeam_channel::unbounded(); - let project_path = format!("{}/tests/resources/is-cdylib", env!("CARGO_MANIFEST_DIR")); assert!(matches!( - build_crate(Default::default(), Path::new(&project_path), false, tx).await, + build_crate(Path::new(&project_path), false, tx).await, Ok(Runtime::Legacy(_)) )); assert!(PathBuf::from(project_path) - .join("target/debug/libis_cdylib.so") + .join("target/debug/is-bin") .exists()); } @@ -70,7 +45,7 @@ async fn not_found() { "{}/tests/resources/non-existing", env!("CARGO_MANIFEST_DIR") ); - build_crate(Default::default(), Path::new(&project_path), false, tx) + build_crate(Path::new(&project_path), false, tx) .await .unwrap(); } diff --git a/service/tests/integration/helpers/loader.rs b/service/tests/integration/helpers/loader.rs deleted file mode 100644 index 2d09238c2..000000000 --- a/service/tests/integration/helpers/loader.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::path::PathBuf; -use std::process::Command; - -use shuttle_service::loader::{Loader, LoaderError}; - -pub fn build_so_create_loader(resources: &str, crate_name: &str) -> Result { - let crate_dir: PathBuf = [resources, crate_name].iter().collect(); - - Command::new("cargo") - .args(["build", "--release"]) - .current_dir(&crate_dir) - .spawn() - .unwrap() - .wait() - .unwrap(); - - let dashes_replaced = crate_name.replace('-', "_"); - - let lib_name = if cfg!(target_os = "windows") { - format!("{}.dll", dashes_replaced) - } else { - format!("lib{}.so", dashes_replaced) - }; - - let so_path = crate_dir.join("target/release").join(lib_name); - - Loader::from_so_file(so_path) -} diff --git a/service/tests/integration/helpers/mod.rs b/service/tests/integration/helpers/mod.rs deleted file mode 100644 index e9b2e29f0..000000000 --- a/service/tests/integration/helpers/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod sqlx; - -#[cfg(feature = "loader")] -pub mod loader; diff --git a/service/tests/integration/helpers/sqlx.rs b/service/tests/integration/helpers/sqlx.rs deleted file mode 100644 index 79632e967..000000000 --- a/service/tests/integration/helpers/sqlx.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::future::Future; -use std::process::Command; -use std::thread::sleep; -use std::time::Duration; - -use portpicker::pick_unused_port; -use sqlx::Connection; - -pub struct PostgresInstance { - port: u16, - container: String, - password: String, -} - -impl Default for PostgresInstance { - fn default() -> Self { - Self::new() - } -} - -impl PostgresInstance { - /// Creates a new [`PostgresInstance`] using the official postgres:11 docker image - /// - /// Does not wait for the container to be ready. Use [`PostgresInstance::wait_for_ready`] and - /// [`PostgresInstance::wait_for_connectable`] for that. - pub fn new() -> Self { - let port = pick_unused_port().expect("could not find a free port for postgres"); - let container = "postgres-shuttle-service-integration-test".to_string(); - let password = "password".to_string(); - - Command::new("docker") - .args([ - "run", - "--name", - &container, - "-e", - &format!("POSTGRES_PASSWORD={}", password), - "-p", - &format!("{}:5432", port), - "postgres:11", // Our Containerfile image is based on buster which has postgres version 11 - ]) - .spawn() - .expect("failed to start a postgres instance"); - - Self { - port, - container, - password, - } - } - - pub fn get_uri(&self) -> String { - format!( - "postgres://postgres:{}@localhost:{}", - self.password, self.port - ) - } - - pub fn wait_for_connectable(&self) -> impl Future + '_ { - self.async_wait_for(|instance| { - let uri = instance.get_uri().as_str().to_string(); - async move { sqlx::PgConnection::connect(uri.as_str()).await.is_ok() } - }) - } - - pub async fn async_wait_for(&self, f: F) - where - F: Fn(&Self) -> Fut, - Fut: Future, - { - let mut timeout = 20 * 10; - - while timeout > 0 { - timeout -= 1; - - if f(self).await { - return; - } - - sleep(Duration::from_millis(100)); - } - - panic!("timed out waiting for PostgresInstance"); - } - - pub fn wait_for_ready(&self) { - self.wait_for(|instance| { - let status = Command::new("docker") - .args(["exec", &instance.container, "pg_isready"]) - .output() - .expect("failed to get postgres ready status") - .status; - - status.success() - }) - } - - pub fn wait_for(&self, f: F) - where - F: Fn(&Self) -> bool, - { - let mut timeout = 20 * 10; - - while timeout > 0 { - timeout -= 1; - - if f(self) { - return; - } - - sleep(Duration::from_millis(100)); - } - - panic!("timed out waiting for PostgresInstance"); - } -} - -impl Drop for PostgresInstance { - fn drop(&mut self) { - Command::new("docker") - .args(["stop", &self.container]) - .spawn() - .expect("failed to spawn stop for postgres container") - .wait() - .expect("postgres container stop failed"); - - Command::new("docker") - .args(["rm", &self.container]) - .spawn() - .expect("failed to spawn stop for remove container") - .wait() - .expect("postgres container remove failed"); - } -} diff --git a/service/tests/integration/loader.rs b/service/tests/integration/loader.rs deleted file mode 100644 index ba42c3807..000000000 --- a/service/tests/integration/loader.rs +++ /dev/null @@ -1,192 +0,0 @@ -use crate::helpers::{loader::build_so_create_loader, sqlx::PostgresInstance}; - -use shuttle_common::log::Level; -use shuttle_common::LogItem; -use shuttle_service::loader::LoaderError; -use shuttle_service::{database, Error, Factory, Logger, ServiceName}; -use std::collections::BTreeMap; -use std::net::{Ipv4Addr, SocketAddr}; -use std::process::exit; -use std::str::FromStr; -use std::time::Duration; -use tokio::sync::mpsc::{self, UnboundedReceiver}; - -use async_trait::async_trait; - -const RESOURCES_PATH: &str = "tests/resources"; - -struct DummyFactory { - postgres_instance: Option, - service_name: ServiceName, -} - -impl DummyFactory { - fn new() -> Self { - Self { - postgres_instance: None, - service_name: ServiceName::from_str("test").unwrap(), - } - } -} - -fn get_logger() -> (Logger, UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded_channel(); - let logger = Logger::new(tx, Default::default()); - - (logger, rx) -} - -#[async_trait] -impl Factory for DummyFactory { - fn get_service_name(&self) -> ServiceName { - self.service_name.clone() - } - - async fn get_db_connection_string(&mut self, _: database::Type) -> Result { - let uri = if let Some(postgres_instance) = &self.postgres_instance { - postgres_instance.get_uri() - } else { - let postgres_instance = PostgresInstance::new(); - postgres_instance.wait_for_ready(); - postgres_instance.wait_for_connectable().await; - let uri = postgres_instance.get_uri(); - self.postgres_instance = Some(postgres_instance); - uri - }; - - Ok(uri) - } - - async fn get_secrets(&mut self) -> Result, Error> { - panic!("did not expect any loader test to get secrets") - } - - fn get_build_path(&self) -> Result { - panic!("did not expect any loader test to get the build path") - } - - fn get_storage_path(&self) -> Result { - panic!("did not expect any loader test to get the storage path") - } -} - -#[test] -fn not_shuttle() { - let result = build_so_create_loader(RESOURCES_PATH, "not-shuttle"); - assert!(matches!(result, Err(LoaderError::GetEntrypoint(_)))); -} - -#[tokio::test] -async fn sleep_async() { - let loader = build_so_create_loader(RESOURCES_PATH, "sleep-async").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - let (handler, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - // Give service some time to start up - tokio::time::sleep(Duration::from_secs(1)).await; - - tokio::spawn(async { - // Time is less than sleep in service - tokio::time::sleep(Duration::from_secs(5)).await; - println!("Test failed as async service was not aborted"); - exit(1); - }); - - handler.abort(); - assert!(handler.await.unwrap_err().is_cancelled()); -} - -#[tokio::test] -async fn sleep() { - let loader = build_so_create_loader(RESOURCES_PATH, "sleep").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - let (handler, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - // Give service some time to start up - tokio::time::sleep(Duration::from_secs(1)).await; - - tokio::spawn(async { - // Time is less than sleep in service - tokio::time::sleep(Duration::from_secs(5)).await; - println!("Test failed as blocking service was not aborted"); - exit(1); - }); - - handler.abort(); - assert!(handler.await.unwrap_err().is_cancelled()); -} - -#[tokio::test] -async fn sqlx_pool() { - let loader = build_so_create_loader(RESOURCES_PATH, "sqlx-pool").unwrap(); - - // Make sure we'll get a log entry - std::env::set_var("RUST_LOG", "info"); - - // Don't initialize a pre-existing PostgresInstance here because the `PostgresInstance::wait_for_connectable()` - // code has `awaits` and we want to make sure they do not block inside `Service::build()`. - // At the same time we also want to test the PgPool is created on the correct runtime (ie does not cause a - // "has to run on a tokio runtime" error) - let mut factory = DummyFactory::new(); - - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, mut rx) = get_logger(); - let (handler, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - handler.await.unwrap().unwrap(); - - let log = rx.recv().await.unwrap(); - let value = serde_json::from_slice::(&log.fields).unwrap(); - let message = value - .as_object() - .unwrap() - .get("message") - .unwrap() - .as_str() - .unwrap(); - assert!( - message.starts_with("SELECT 'Hello world';"), - "got: {}", - message - ); - assert_eq!(log.target, "sqlx::query"); - assert_eq!(log.level, Level::Info); -} - -#[tokio::test] -async fn build_panic() { - let loader = build_so_create_loader(RESOURCES_PATH, "build-panic").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - - if let Err(Error::BuildPanic(msg)) = loader.load(&mut factory, addr, logger).await { - assert_eq!(&msg, "panic in build"); - } else { - panic!("expected `Err(Error::BuildPanic(_))`"); - } -} - -#[tokio::test] -async fn bind_panic() { - let loader = build_so_create_loader(RESOURCES_PATH, "bind-panic").unwrap(); - - let mut factory = DummyFactory::new(); - let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), 8001); - let (logger, _rx) = get_logger(); - - let (handle, _) = loader.load(&mut factory, addr, logger).await.unwrap(); - - if let Err(Error::BindPanic(msg)) = handle.await.unwrap() { - assert_eq!(&msg, "panic in bind"); - } else { - panic!("expected `Err(Error::BindPanic(_))`"); - } -} diff --git a/service/tests/integration/main.rs b/service/tests/integration/main.rs index b09409f82..f8f5ab1f1 100644 --- a/service/tests/integration/main.rs +++ b/service/tests/integration/main.rs @@ -1,7 +1,2 @@ -pub mod helpers; - -#[cfg(feature = "loader")] -mod loader; - -#[cfg(feature = "loader")] +#[cfg(feature = "builder")] mod build_crate; diff --git a/service/tests/resources/bind-panic/src/lib.rs b/service/tests/resources/bind-panic/src/lib.rs deleted file mode 100644 index 1ecd700bf..000000000 --- a/service/tests/resources/bind-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - panic!("panic in bind"); - } -} - -#[shuttle_service::main] -async fn bind_panic() -> Result { - Ok(MyService) -} diff --git a/service/tests/resources/build-panic/Cargo.toml b/service/tests/resources/build-panic/Cargo.toml deleted file mode 100644 index 93a0b14e8..000000000 --- a/service/tests/resources/build-panic/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "build-panic" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -shuttle-service = { path = "../../../" } diff --git a/service/tests/resources/build-panic/src/lib.rs b/service/tests/resources/build-panic/src/lib.rs deleted file mode 100644 index 084e5c802..000000000 --- a/service/tests/resources/build-panic/src/lib.rs +++ /dev/null @@ -1,18 +0,0 @@ -use shuttle_service::Service; - -struct MyService; - -#[shuttle_service::async_trait] -impl Service for MyService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::Error> { - Ok(()) - } -} - -#[shuttle_service::main] -async fn build_panic() -> Result { - panic!("panic in build"); -} diff --git a/service/tests/resources/is-bin/Cargo.toml b/service/tests/resources/is-bin/Cargo.toml new file mode 100644 index 000000000..52abbeab9 --- /dev/null +++ b/service/tests/resources/is-bin/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "is-bin" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +axum = "0.6.0" +shuttle-runtime = { path = "../../../../runtime" } +shuttle-service = { path = "../../../", features = ["web-axum"] } +tokio = { version = "1.22.0" } diff --git a/service/tests/resources/is-bin/src/main.rs b/service/tests/resources/is-bin/src/main.rs new file mode 100644 index 000000000..21ffd2090 --- /dev/null +++ b/service/tests/resources/is-bin/src/main.rs @@ -0,0 +1,6 @@ +#[shuttle_service::main] +async fn axum() -> shuttle_service::ShuttleAxum { + let router = axum::Router::new(); + + Ok(router) +} diff --git a/service/tests/resources/is-cdylib/src/lib.rs b/service/tests/resources/is-cdylib/src/lib.rs deleted file mode 100644 index 80a1cf666..000000000 --- a/service/tests/resources/is-cdylib/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[shuttle_service::main] -async fn rocket() -> shuttle_service::ShuttleRocket { - let rocket = rocket::build(); - Ok(rocket) -} diff --git a/service/tests/resources/is-cdylib/Cargo.toml b/service/tests/resources/not-bin/Cargo.toml similarity index 75% rename from service/tests/resources/is-cdylib/Cargo.toml rename to service/tests/resources/not-bin/Cargo.toml index 99083de80..8d34c0b74 100644 --- a/service/tests/resources/is-cdylib/Cargo.toml +++ b/service/tests/resources/not-bin/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "is-cdylib" +name = "not-bin" version = "0.1.0" edition = "2021" @@ -11,3 +11,4 @@ crate-type = ["cdylib", "staticlib"] [dependencies] rocket = "0.5.0-rc.2" shuttle-service = { path = "../../../", features = ["web-rocket"] } +shuttle-runtime = { path = "../../../../runtime" } diff --git a/service/tests/resources/not-cdylib/src/lib.rs b/service/tests/resources/not-bin/src/lib.rs similarity index 71% rename from service/tests/resources/not-cdylib/src/lib.rs rename to service/tests/resources/not-bin/src/lib.rs index 80a1cf666..329aed72c 100644 --- a/service/tests/resources/not-cdylib/src/lib.rs +++ b/service/tests/resources/not-bin/src/lib.rs @@ -1,3 +1,5 @@ +// This will fail to compile since it's a library. + #[shuttle_service::main] async fn rocket() -> shuttle_service::ShuttleRocket { let rocket = rocket::build(); diff --git a/service/tests/resources/not-cdylib/Cargo.toml b/service/tests/resources/not-cdylib/Cargo.toml deleted file mode 100644 index 47a2afa53..000000000 --- a/service/tests/resources/not-cdylib/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "not-cdylib" -version = "0.1.0" -edition = "2021" - -[workspace] - -[lib] -crate-type = ["staticlib"] - -[dependencies] -rocket = "0.5.0-rc.2" -shuttle-service = { path = "../../../", features = ["web-rocket"] } diff --git a/service/tests/resources/not-lib/Cargo.toml b/service/tests/resources/not-lib/Cargo.toml deleted file mode 100644 index 3c2b2f01c..000000000 --- a/service/tests/resources/not-lib/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "not-lib" -version = "0.1.0" -edition = "2021" - -[workspace] diff --git a/service/tests/resources/not-lib/src/main.rs b/service/tests/resources/not-lib/src/main.rs deleted file mode 100644 index 2fc1633f9..000000000 --- a/service/tests/resources/not-lib/src/main.rs +++ /dev/null @@ -1,5 +0,0 @@ -// This will fail to compile since it is missing a `[lib]` section in its Cargo.toml file. - -fn main() { - println!("this is not valid as it is not a library!"); -} diff --git a/service/tests/resources/not-shuttle/Cargo.toml b/service/tests/resources/not-shuttle/Cargo.toml index b7fe25901..86af40f54 100644 --- a/service/tests/resources/not-shuttle/Cargo.toml +++ b/service/tests/resources/not-shuttle/Cargo.toml @@ -3,10 +3,10 @@ name = "not-shuttle" version = "0.1.0" edition = "2021" -[lib] -crate-type = ["cdylib"] - [workspace] [dependencies] -shuttle-service = "0.8.0" +axum = "0.6.0" +shuttle-runtime = { path = "../../../../runtime" } +shuttle-service = { path = "../../../", features = ["web-axum"] } +tokio = { version = "1.22.0" } diff --git a/service/tests/resources/not-shuttle/src/lib.rs b/service/tests/resources/not-shuttle/src/lib.rs deleted file mode 100644 index b25ff369e..000000000 --- a/service/tests/resources/not-shuttle/src/lib.rs +++ /dev/null @@ -1 +0,0 @@ -// This service cannot be hosted on shuttle since it is missing the entrypoint the shutlle macros would have added!!! diff --git a/service/tests/resources/not-shuttle/src/main.rs b/service/tests/resources/not-shuttle/src/main.rs new file mode 100644 index 000000000..9cf9d865b --- /dev/null +++ b/service/tests/resources/not-shuttle/src/main.rs @@ -0,0 +1,6 @@ +// This service cannot be hosted on shuttle since it is missing the runtime the shuttle main macro would have added!!! +async fn axum() -> shuttle_service::ShuttleAxum { + let router = axum::Router::new(); + + Ok(router) +} diff --git a/service/tests/resources/sleep-async/Cargo.toml b/service/tests/resources/sleep-async/Cargo.toml deleted file mode 100644 index e9fa47aae..000000000 --- a/service/tests/resources/sleep-async/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "sleep-async" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -tokio = { version = "1.22.0", features = ["time"] } -shuttle-service = { path = "../../../" } diff --git a/service/tests/resources/sleep-async/src/lib.rs b/service/tests/resources/sleep-async/src/lib.rs deleted file mode 100644 index 585537560..000000000 --- a/service/tests/resources/sleep-async/src/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::time::Duration; - -use shuttle_service::Service; -use tokio::time::sleep; - -struct SleepService { - duration: u64, -} - -#[shuttle_service::main] -async fn simple() -> Result { - Ok(SleepService { duration: 10 }) -} - -#[shuttle_service::async_trait] -impl Service for SleepService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - let duration = Duration::from_secs(self.duration); - - sleep(duration).await; - Ok(()) - } -} diff --git a/service/tests/resources/sleep/Cargo.toml b/service/tests/resources/sleep/Cargo.toml deleted file mode 100644 index 40b0f6d1f..000000000 --- a/service/tests/resources/sleep/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "sleep" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -shuttle-service = { path = "../../../" } diff --git a/service/tests/resources/sleep/src/lib.rs b/service/tests/resources/sleep/src/lib.rs deleted file mode 100644 index ac67c50ae..000000000 --- a/service/tests/resources/sleep/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::{thread::sleep, time::Duration}; - -use shuttle_service::Service; - -struct SleepService { - duration: u64, -} - -#[shuttle_service::main] -async fn simple() -> Result { - Ok(SleepService { duration: 10 }) -} - -#[shuttle_service::async_trait] -impl Service for SleepService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - let duration = Duration::from_secs(self.duration); - - sleep(duration); - Ok(()) - } -} diff --git a/service/tests/resources/sqlx-pool/Cargo.toml b/service/tests/resources/sqlx-pool/Cargo.toml deleted file mode 100644 index 14487f75a..000000000 --- a/service/tests/resources/sqlx-pool/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "sqlx-pool" -version = "0.1.0" -edition = "2021" - -[lib] -crate-type = ["cdylib"] - -[workspace] - -[dependencies] -shuttle-service = { path = "../../../" } -shuttle-shared-db = { path = "../../../../resources/shared-db", features = ["postgres"] } -sqlx = { version = "0.6.2", features = [ "runtime-tokio-native-tls" ] } diff --git a/service/tests/resources/sqlx-pool/src/lib.rs b/service/tests/resources/sqlx-pool/src/lib.rs deleted file mode 100644 index a3ae35690..000000000 --- a/service/tests/resources/sqlx-pool/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -use shuttle_service::error::CustomError; -use shuttle_service::Service; -use sqlx::PgPool; - -struct PoolService { - pool: PgPool, -} - -#[shuttle_service::main] -async fn init( - #[shuttle_shared_db::Postgres] pool: PgPool, -) -> Result { - Ok(PoolService { pool }) -} - -impl PoolService { - async fn start(&self) -> Result<(), shuttle_service::error::CustomError> { - let (rec,): (String,) = sqlx::query_as("SELECT 'Hello world'") - .fetch_one(&self.pool) - .await - .map_err(CustomError::new)?; - - assert_eq!(rec, "Hello world"); - - Ok(()) - } -} - -#[shuttle_service::async_trait] -impl Service for PoolService { - async fn bind( - mut self: Box, - _: std::net::SocketAddr, - ) -> Result<(), shuttle_service::error::Error> { - self.start().await?; - - Ok(()) - } -}