From 4653ef4a613371faab72c4ac27287871d126f59f Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Fri, 7 Nov 2025 02:06:34 -0500 Subject: [PATCH] Use `dockertest` for the newly added `serai-client-serai` test --- .github/workflows/tests.yml | 1 + .gitignore | 1 - Cargo.toml | 1 + deny.toml | 1 + orchestration/src/serai.rs | 9 ---- substrate/client/serai/Cargo.toml | 3 ++ substrate/client/serai/tests/blockchain.rs | 20 ++++++-- tests/docker/Cargo.toml | 1 + tests/docker/src/lib.rs | 58 +++++++++++++++------- tests/substrate/Cargo.toml | 25 ++++++++++ tests/substrate/LICENSE | 15 ++++++ tests/substrate/src/lib.rs | 42 ++++++++++++++++ 12 files changed, 144 insertions(+), 33 deletions(-) create mode 100644 tests/substrate/Cargo.toml create mode 100644 tests/substrate/LICENSE create mode 100644 tests/substrate/src/lib.rs diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 90a96450..6a47f95a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -95,6 +95,7 @@ jobs: -p serai-in-instructions-pallet \ -p serai-runtime \ -p serai-node + -p serai-substrate-tests test-serai-client: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 32936018..4ce66d06 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ Cargo.lock # Don't commit any `Dockerfile`, as they're auto-generated, except the only one which isn't Dockerfile -Dockerfile.fast-epoch !orchestration/runtime/Dockerfile .test-logs diff --git a/Cargo.toml b/Cargo.toml index 7351e594..402a7215 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ members = [ "tests/message-queue", # TODO "tests/processor", # TODO "tests/coordinator", + "tests/substrate", # TODO "tests/full-stack", "tests/reproducible-runtime", ] diff --git a/deny.toml b/deny.toml index ecc3c08f..3f295bd3 100644 --- a/deny.toml +++ b/deny.toml @@ -108,6 +108,7 @@ exceptions = [ { allow = ["AGPL-3.0-only"], name = "serai-message-queue-tests" }, { allow = ["AGPL-3.0-only"], name = "serai-processor-tests" }, { allow = ["AGPL-3.0-only"], name = "serai-coordinator-tests" }, + { allow = ["AGPL-3.0-only"], name = "serai-substrate-tests" }, { allow = ["AGPL-3.0-only"], name = "serai-full-stack-tests" }, { allow = ["AGPL-3.0-only"], name = "serai-reproducible-runtime-tests" }, ] diff --git a/orchestration/src/serai.rs b/orchestration/src/serai.rs index b963adc2..a07afce9 100644 --- a/orchestration/src/serai.rs +++ b/orchestration/src/serai.rs @@ -13,8 +13,6 @@ pub fn serai( ) { // Always builds in release for performance reasons let setup = mimalloc(Os::Debian).to_string() + &build_serai_service("", true, "", "serai-node"); - let setup_fast_epoch = - mimalloc(Os::Debian).to_string() + &build_serai_service("", true, "fast-epoch", "serai-node"); let env_vars = [("KEY", hex::encode(serai_key.to_repr()))]; let mut env_vars_str = String::new(); @@ -39,16 +37,9 @@ CMD {env_vars_str} "/run.sh" let run = os(Os::Debian, "", "serai") + &run_serai; let res = setup + &run; - let res_fast_epoch = setup_fast_epoch + &run; let mut serai_path = orchestration_path.to_path_buf(); serai_path.push("serai"); - - let mut serai_fast_epoch_path = serai_path.clone(); - serai_path.push("Dockerfile"); - serai_fast_epoch_path.push("Dockerfile.fast-epoch"); - write_dockerfile(serai_path, &res); - write_dockerfile(serai_fast_epoch_path, &res_fast_epoch); } diff --git a/substrate/client/serai/Cargo.toml b/substrate/client/serai/Cargo.toml index 80e11258..d2ff002b 100644 --- a/substrate/client/serai/Cargo.toml +++ b/substrate/client/serai/Cargo.toml @@ -30,3 +30,6 @@ async-lock = "3" [dev-dependencies] tokio = { version = "1", default-features = false, features = ["rt", "macros"] } +dockertest = "0.5" +serai-docker-tests = { path = "../../../tests/docker" } +serai-substrate-tests = { path = "../../../tests/substrate" } diff --git a/substrate/client/serai/tests/blockchain.rs b/substrate/client/serai/tests/blockchain.rs index 6cbcbaac..cdede8d7 100644 --- a/substrate/client/serai/tests/blockchain.rs +++ b/substrate/client/serai/tests/blockchain.rs @@ -1,9 +1,19 @@ use serai_client_serai::*; #[tokio::test] -async fn main() { - let serai = Serai::new("http://127.0.0.1:9944".to_string()).unwrap(); - let block = serai.block_by_number(0).await.unwrap(); - assert_eq!(serai.block(block.header.hash()).await.unwrap(), block); - assert!(serai.finalized(block.header.hash()).await.unwrap()); +async fn blockchain() { + let mut test = dockertest::DockerTest::new(); + let (composition, handle) = serai_substrate_tests::composition( + "alice", + serai_docker_tests::fresh_logs_folder(true, "serai-client/blockchain"), + ); + test.provide_container(composition); + test + .run_async(async |ops| { + let serai = serai_substrate_tests::rpc(&ops, handle).await; + let block = serai.block_by_number(0).await.unwrap(); + assert_eq!(serai.block(block.header.hash()).await.unwrap(), block); + assert!(serai.finalized(block.header.hash()).await.unwrap()); + }) + .await; } diff --git a/tests/docker/Cargo.toml b/tests/docker/Cargo.toml index 1b3a8a6e..82cd02b7 100644 --- a/tests/docker/Cargo.toml +++ b/tests/docker/Cargo.toml @@ -18,3 +18,4 @@ workspace = true [dependencies] chrono = "0.4" +dockertest = "0.5" diff --git a/tests/docker/src/lib.rs b/tests/docker/src/lib.rs index 3db069d8..5ed137c9 100644 --- a/tests/docker/src/lib.rs +++ b/tests/docker/src/lib.rs @@ -1,5 +1,5 @@ use std::{ - sync::{Mutex, OnceLock}, + sync::{Mutex, LazyLock}, collections::{HashSet, HashMap}, time::SystemTime, path::PathBuf, @@ -7,6 +7,16 @@ use std::{ process::Command, }; +use dockertest::{LogSource, LogAction, LogPolicy, LogOptions}; + +pub fn handle(desc: &str) -> String { + static UNIQUE_ID: LazyLock> = LazyLock::new(|| Mutex::new(0)); + let mut unique_id_lock = UNIQUE_ID.lock().unwrap(); + let unique_id = *unique_id_lock; + *unique_id_lock += 1; + format!("{desc}-{unique_id}") +} + pub fn fresh_logs_folder(first: bool, label: &str) -> String { let logs_path = [std::env::current_dir().unwrap().to_str().unwrap(), ".test-logs", label] .iter() @@ -22,30 +32,33 @@ pub fn fresh_logs_folder(first: bool, label: &str) -> String { logs_path.to_str().unwrap().to_string() } -// TODO: Merge this with what's in serai-orchestrator/have serai-orchestrator perform building -static BUILT: OnceLock>> = OnceLock::new(); +pub fn log_options(path: String) -> LogOptions { + LogOptions { + action: if std::env::var("GITHUB_CI") == Ok("true".to_string()) { + LogAction::Forward + } else { + LogAction::ForwardToFile { path } + }, + policy: LogPolicy::Always, + source: LogSource::Both, + } +} + +// TODO: Should `serai-orchestrator` handle building? pub fn build(name: String) { - let built = BUILT.get_or_init(|| Mutex::new(HashMap::new())); + static BUILT: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); // Only one call to build will acquire this lock - let mut built_lock = built.lock().unwrap(); + let mut built_lock = BUILT.lock().unwrap(); if built_lock.contains_key(&name) { // If it was built, return return; } // Else, hold the lock while we build - let mut repo_path = env::current_exe().unwrap(); - repo_path.pop(); - assert!(repo_path.as_path().ends_with("deps")); - repo_path.pop(); - assert!(repo_path.as_path().ends_with("debug")); - repo_path.pop(); - assert!(repo_path.as_path().ends_with("target")); - repo_path.pop(); // Run the orchestrator to ensure the most recent files exist if !Command::new("cargo") - .current_dir(&repo_path) .arg("run") .arg("-p") .arg("serai-orchestrator") @@ -62,7 +75,6 @@ pub fn build(name: String) { } if !Command::new("cargo") - .current_dir(&repo_path) .arg("run") .arg("-p") .arg("serai-orchestrator") @@ -78,6 +90,18 @@ pub fn build(name: String) { panic!("failed to run the orchestrator"); } + let mut repo_path = PathBuf::from( + core::str::from_utf8( + &Command::new("cargo") + .args(["locate-project", "--workspace", "--message-format", "plain"]) + .output() + .expect("couldn't locate workspace with `cargo`") + .stdout, + ) + .expect("`cargo` outputted non-UTF-8 bytes to `stdout`"), + ); + repo_path.pop(); // Pop the `Cargo.toml` term + let mut orchestration_path = repo_path.clone(); orchestration_path.push("orchestration"); if name != "runtime" { @@ -91,8 +115,6 @@ pub fn build(name: String) { if name.contains("-processor") { dockerfile_path = dockerfile_path.join("processor").join(name.split('-').next().unwrap()).join("Dockerfile"); - } else if name == "serai-fast-epoch" { - dockerfile_path = dockerfile_path.join("serai").join("Dockerfile.fast-epoch"); } else { dockerfile_path = dockerfile_path.join(&name).join("Dockerfile"); } @@ -150,7 +172,7 @@ pub fn build(name: String) { meta(repo_path.join("message-queue")), meta(repo_path.join("coordinator")), ], - "runtime" | "serai" | "serai-fast-epoch" => vec![ + "runtime" | "serai" => vec![ meta(repo_path.join("common")), meta(repo_path.join("crypto")), meta(repo_path.join("substrate")), diff --git a/tests/substrate/Cargo.toml b/tests/substrate/Cargo.toml new file mode 100644 index 00000000..5cb20c00 --- /dev/null +++ b/tests/substrate/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "serai-substrate-tests" +version = "0.1.0" +description = "Tests for Serai's node" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/tests/substrate" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +serai-client-serai = { path = "../../substrate/client/serai" } + +tokio = { version = "1", features = ["time"] } + +dockertest = "0.5" +serai-docker-tests = { path = "../docker" } diff --git a/tests/substrate/LICENSE b/tests/substrate/LICENSE new file mode 100644 index 00000000..621233a9 --- /dev/null +++ b/tests/substrate/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2023-2025 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/tests/substrate/src/lib.rs b/tests/substrate/src/lib.rs new file mode 100644 index 00000000..45b750d8 --- /dev/null +++ b/tests/substrate/src/lib.rs @@ -0,0 +1,42 @@ +use std::time::Duration; + +use serai_client_serai::Serai; + +use dockertest::{StartPolicy, PullPolicy, Image, TestBodySpecification, DockerOperations}; + +pub struct Handle(String); + +pub fn composition(name: &str, logs_path: String) -> (TestBodySpecification, Handle) { + let handle = serai_docker_tests::handle(&format!("serai-{name}")); + serai_docker_tests::build("serai".to_string()); + ( + TestBodySpecification::with_image( + Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never), + ) + .replace_env( + [("SERAI_NAME".to_string(), name.to_lowercase()), ("KEY".to_string(), " ".to_string())] + .into(), + ) + .set_start_policy(StartPolicy::Strict) + .set_publish_all_ports(true) + .set_handle(handle.clone()) + .set_log_options(Some(serai_docker_tests::log_options(logs_path))), + Handle(handle), + ) +} + +pub async fn rpc(ops: &DockerOperations, handle: Handle) -> Serai { + let serai_rpc = ops.handle(&handle.0).host_port(9944).unwrap(); + let serai_rpc = format!("http://{}:{}", serai_rpc.0, serai_rpc.1); + + // If the RPC server has yet to start, sleep for up to 60s until it does + let client = Serai::new(serai_rpc.clone()).unwrap(); + for _ in 0 .. 180 { + tokio::time::sleep(Duration::from_secs(1)).await; + if client.block_by_number(0).await.is_err() { + continue; + } + return client; + } + panic!("serai RPC server wasn't available after 60s"); +}