diff --git a/.github/workflows/gnd-binary-build.yml b/.github/workflows/gnd-binary-build.yml
index 7622d608a91..ca31d660263 100644
--- a/.github/workflows/gnd-binary-build.yml
+++ b/.github/workflows/gnd-binary-build.yml
@@ -109,7 +109,7 @@ jobs:
certificate-data: ${{ secrets.APPLE_CERT_DATA }}
certificate-password: ${{ secrets.APPLE_CERT_PASSWORD }}
certificate-id: ${{ secrets.APPLE_TEAM_ID }}
- options: --options runtime
+ options: --options runtime --entitlements entitlements.plist
- name: Notarize macOS binary
if: startsWith(matrix.runner, 'macos')
diff --git a/Cargo.lock b/Cargo.lock
index 51ae390703d..e17159fcdbd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2222,6 +2222,7 @@ dependencies = [
"openssl",
"postgres",
"postgres-openssl",
+ "pq-sys",
"pretty_assertions",
"rand 0.9.1",
"serde",
@@ -3809,7 +3810,7 @@ dependencies = [
[[package]]
name = "pgtemp"
version = "0.6.0"
-source = "git+https://github.com/incrypto32/pgtemp?branch=initdb-args#929a9f96eab841d880c2ebf280e00054ca55ec0e"
+source = "git+https://github.com/graphprotocol/pgtemp?branch=initdb-args#08a95d441d74ce0a50b6e0a55dbf96d8362d8fb7"
dependencies = [
"libc",
"tempfile",
@@ -3940,12 +3941,23 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+[[package]]
+name = "pq-src"
+version = "0.3.8+libpq-17.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ee4b24aec6c54ba0995bcc9ba17aa369db8764122980c5dbd3c70d26206dc73"
+dependencies = [
+ "cc",
+ "openssl-sys",
+]
+
[[package]]
name = "pq-sys"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a24ff9e4cf6945c988f0db7005d87747bf72864965c3529d259ad155ac41d584"
dependencies = [
+ "pq-src",
"vcpkg",
]
diff --git a/Cargo.toml b/Cargo.toml
index 78694e06d1c..66a6948e115 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -55,6 +55,7 @@ diesel = { version = "2.2.7", features = [
"chrono",
"i-implement-a-third-party-backend-and-opt-into-breaking-changes",
] }
+pq-sys = { version = "0.6", features = ["bundled"] }
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
diesel-dynamic-schema = { version = "0.2.3", features = ["postgres"] }
diesel_derives = "2.2.3"
diff --git a/entitlements.plist b/entitlements.plist
new file mode 100644
index 00000000000..d9ce520f2e1
--- /dev/null
+++ b/entitlements.plist
@@ -0,0 +1,12 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-executable-page-protection
+
+
+
\ No newline at end of file
diff --git a/node/Cargo.toml b/node/Cargo.toml
index 16393c36af5..7a6ea1a5914 100644
--- a/node/Cargo.toml
+++ b/node/Cargo.toml
@@ -48,4 +48,4 @@ globset = "0.4.16"
notify = "8.0.0"
[target.'cfg(unix)'.dependencies]
-pgtemp = { git = "https://github.com/incrypto32/pgtemp", branch = "initdb-args" }
+pgtemp = { git = "https://github.com/graphprotocol/pgtemp", branch = "initdb-args" }
diff --git a/node/src/bin/dev.rs b/node/src/bin/dev.rs
index 27020ee3eb3..cad3dc166f1 100644
--- a/node/src/bin/dev.rs
+++ b/node/src/bin/dev.rs
@@ -1,4 +1,4 @@
-use std::{mem, path::Path, sync::Arc};
+use std::{path::Path, sync::Arc};
use anyhow::{Context, Result};
use clap::Parser;
@@ -13,6 +13,7 @@ use graph::{
};
use graph_core::polling_monitor::ipfs_service;
use graph_node::{
+ dev::watcher,
dev::watcher::{parse_manifest_args, watch_subgraphs},
launcher,
opt::Opt,
@@ -20,7 +21,15 @@ use graph_node::{
use lazy_static::lazy_static;
#[cfg(unix)]
-use pgtemp::PgTempDBBuilder;
+use pgtemp::{PgTempDB, PgTempDBBuilder};
+
+// Add an alias for the temporary Postgres DB handle. On non unix
+// targets we don’t have pgtemp, but we still need the type to satisfy the
+// function signatures.
+#[cfg(unix)]
+type TempPgDB = PgTempDB;
+#[cfg(not(unix))]
+type TempPgDB = ();
git_testament!(TESTAMENT);
lazy_static! {
@@ -90,6 +99,35 @@ pub struct DevOpt {
default_value = "https://api.thegraph.com/ipfs"
)]
pub ipfs: Vec,
+ #[clap(
+ long,
+ default_value = "8000",
+ value_name = "PORT",
+ help = "Port for the GraphQL HTTP server",
+ env = "GRAPH_GRAPHQL_HTTP_PORT"
+ )]
+ pub http_port: u16,
+ #[clap(
+ long,
+ default_value = "8030",
+ value_name = "PORT",
+ help = "Port for the index node server"
+ )]
+ pub index_node_port: u16,
+ #[clap(
+ long,
+ default_value = "8020",
+ value_name = "PORT",
+ help = "Port for the JSON-RPC admin server"
+ )]
+ pub admin_port: u16,
+ #[clap(
+ long,
+ default_value = "8040",
+ value_name = "PORT",
+ help = "Port for the Prometheus metrics server"
+ )]
+ pub metrics_port: u16,
}
/// Builds the Graph Node options from DevOpt
@@ -109,7 +147,12 @@ fn build_args(dev_opt: &DevOpt, db_url: &str) -> Result {
args.push("--postgres-url".to_string());
args.push(db_url.to_string());
- let opt = Opt::parse_from(args);
+ let mut opt = Opt::parse_from(args);
+
+ opt.http_port = dev_opt.http_port;
+ opt.admin_port = dev_opt.admin_port;
+ opt.metrics_port = dev_opt.metrics_port;
+ opt.index_node_port = dev_opt.index_node_port;
Ok(opt)
}
@@ -118,7 +161,7 @@ async fn run_graph_node(
logger: &Logger,
opt: Opt,
link_resolver: Arc,
- subgraph_updates_channel: Option>,
+ subgraph_updates_channel: mpsc::Receiver<(DeploymentHash, SubgraphName)>,
) -> Result<()> {
let env_vars = Arc::new(EnvVars::from_env().context("Failed to load environment variables")?);
@@ -139,16 +182,19 @@ async fn run_graph_node(
env_vars,
ipfs_service,
link_resolver,
- subgraph_updates_channel,
+ Some(subgraph_updates_channel),
)
.await;
Ok(())
}
/// Get the database URL, either from the provided option or by creating a temporary database
-fn get_database_url(postgres_url: Option<&String>, database_dir: &Path) -> Result {
+fn get_database_url(
+ postgres_url: Option<&String>,
+ database_dir: &Path,
+) -> Result<(String, Option)> {
if let Some(url) = postgres_url {
- Ok(url.clone())
+ Ok((url.clone(), None))
} else {
#[cfg(unix)]
{
@@ -162,13 +208,14 @@ fn get_database_url(postgres_url: Option<&String>, database_dir: &Path) -> Resul
let db = PgTempDBBuilder::new()
.with_data_dir_prefix(database_dir)
- .with_initdb_param("-E", "UTF8")
- .with_initdb_param("--locale", "C")
+ .persist_data(false)
+ .with_initdb_arg("-E", "UTF8")
+ .with_initdb_arg("--locale", "C")
.start();
let url = db.connection_uri().to_string();
- // Prevent the database from being dropped by forgetting it
- mem::forget(db);
- Ok(url)
+ // Return the handle so it lives for the lifetime of the program; dropping it will
+ // shut down Postgres and remove the temporary directory automatically.
+ Ok((url, Some(db)))
}
#[cfg(not(unix))]
@@ -182,6 +229,8 @@ fn get_database_url(postgres_url: Option<&String>, database_dir: &Path) -> Resul
#[tokio::main]
async fn main() -> Result<()> {
+ std::env::set_var("ETHEREUM_REORG_THRESHOLD", "10");
+ std::env::set_var("GRAPH_NODE_DISABLE_DEPLOYMENT_HASH_VALIDATION", "true");
env_logger::init();
let dev_opt = DevOpt::parse();
@@ -189,11 +238,12 @@ async fn main() -> Result<()> {
let logger = logger(true);
- info!(logger, "Starting Graph Node Dev");
+ info!(logger, "Starting Graph Node Dev 1");
info!(logger, "Database directory: {}", database_dir.display());
- // Get the database URL
- let db_url = get_database_url(dev_opt.postgres_url.as_ref(), database_dir)?;
+ // Get the database URL and keep the temporary database handle alive for the life of the
+ // program so that it is dropped (and cleaned up) on graceful shutdown.
+ let (db_url, mut temp_db_opt) = get_database_url(dev_opt.postgres_url.as_ref(), database_dir)?;
let opt = build_args(&dev_opt, &db_url)?;
@@ -201,17 +251,26 @@ async fn main() -> Result<()> {
parse_manifest_args(dev_opt.manifests, dev_opt.sources, &logger)?;
let file_link_resolver = Arc::new(FileLinkResolver::new(None, source_subgraph_aliases.clone()));
- let (tx, rx) = dev_opt.watch.then(|| mpsc::channel(1)).unzip();
+ let (tx, rx) = mpsc::channel(1);
let logger_clone = logger.clone();
graph::spawn(async move {
let _ = run_graph_node(&logger_clone, opt, file_link_resolver, rx).await;
});
- if let Some(tx) = tx {
+ if let Err(e) =
+ watcher::deploy_all_subgraphs(&logger, &manifests_paths, &source_subgraph_aliases, &tx)
+ .await
+ {
+ error!(logger, "Error deploying subgraphs"; "error" => e.to_string());
+ std::process::exit(1);
+ }
+
+ if dev_opt.watch {
+ let logger_clone_watch = logger.clone();
graph::spawn_blocking(async move {
if let Err(e) = watch_subgraphs(
- &logger,
+ &logger_clone_watch,
manifests_paths,
source_subgraph_aliases,
vec!["pgtemp-*".to_string()],
@@ -219,12 +278,27 @@ async fn main() -> Result<()> {
)
.await
{
- error!(logger, "Error watching subgraphs"; "error" => e.to_string());
+ error!(logger_clone_watch, "Error watching subgraphs"; "error" => e.to_string());
std::process::exit(1);
}
});
}
- graph::futures03::future::pending::<()>().await;
+ // Wait for Ctrl+C so we can shut down cleanly and drop the temporary database, which removes
+ // the data directory.
+ tokio::signal::ctrl_c()
+ .await
+ .expect("Failed to listen for Ctrl+C signal");
+ info!(logger, "Received Ctrl+C, shutting down.");
+
+ // Explicitly shut down and clean up the temporary database directory if we started one.
+ #[cfg(unix)]
+ if let Some(db) = temp_db_opt.take() {
+ db.shutdown();
+ }
+
+ std::process::exit(0);
+
+ #[allow(unreachable_code)]
Ok(())
}
diff --git a/node/src/dev/watcher.rs b/node/src/dev/watcher.rs
index 9436db9ac2c..a5e5caa6145 100644
--- a/node/src/dev/watcher.rs
+++ b/node/src/dev/watcher.rs
@@ -255,7 +255,7 @@ fn is_relevant_event(event: &Event, watched_dirs: Vec, exclusion_set: &
}
/// Redeploys all subgraphs in the order it appears in the manifests_paths
-async fn deploy_all_subgraphs(
+pub async fn deploy_all_subgraphs(
logger: &Logger,
manifests_paths: &Vec,
source_subgraph_aliases: &HashMap,
diff --git a/store/postgres/Cargo.toml b/store/postgres/Cargo.toml
index 82a12d823c2..7df8ec5b788 100644
--- a/store/postgres/Cargo.toml
+++ b/store/postgres/Cargo.toml
@@ -9,6 +9,7 @@ blake3 = "1.8"
chrono = { workspace = true }
derive_more = { version = "2.0.1", features = ["full"] }
diesel = { workspace = true }
+pq-sys = { workspace = true}
diesel-dynamic-schema = { workspace = true }
diesel-derive-enum = { workspace = true }
diesel_derives = { workspace = true }
diff --git a/tests/src/config.rs b/tests/src/config.rs
index 6cdd97a216f..46f22b141e7 100644
--- a/tests/src/config.rs
+++ b/tests/src/config.rs
@@ -1,3 +1,4 @@
+use std::sync::OnceLock;
use std::time::{Duration, Instant};
use std::{fs, path::PathBuf};
@@ -13,6 +14,15 @@ use crate::status;
lazy_static! {
pub static ref CONFIG: Config = Config::default();
+ static ref DEV_MODE: OnceLock = OnceLock::new();
+}
+
+pub fn set_dev_mode(val: bool) {
+ DEV_MODE.set(val).expect("DEV_MODE already set");
+}
+
+pub fn dev_mode() -> bool {
+ *DEV_MODE.get().unwrap_or(&false)
}
#[derive(Clone, Debug)]
@@ -117,6 +127,26 @@ impl GraphNodeConfig {
}
}
}
+
+ pub fn from_env() -> Self {
+ if dev_mode() {
+ Self::gnd()
+ } else {
+ Self::default()
+ }
+ }
+
+ fn gnd() -> Self {
+ let bin = fs::canonicalize("../target/debug/gnd")
+ .expect("failed to infer `graph-node` program location. (Was it built already?)");
+
+ Self {
+ bin,
+ ports: GraphNodePorts::default(),
+ ipfs_uri: "http://localhost:3001".to_string(),
+ log_file: TestFile::new("integration-tests/graph-node.log"),
+ }
+ }
}
impl Default for GraphNodeConfig {
@@ -145,6 +175,13 @@ pub struct Config {
impl Config {
pub async fn spawn_graph_node(&self) -> anyhow::Result {
+ self.spawn_graph_node_with_args(&[]).await
+ }
+
+ pub async fn spawn_graph_node_with_args(
+ &self,
+ additional_args: &[&str],
+ ) -> anyhow::Result {
let ports = &self.graph_node.ports;
let args = [
@@ -163,6 +200,12 @@ impl Config {
"--metrics-port",
&ports.metrics.to_string(),
];
+
+ let args = args
+ .iter()
+ .chain(additional_args.iter())
+ .cloned()
+ .collect::>();
let stdout = self.graph_node.log_file.create();
let stderr = stdout.try_clone()?;
status!(
@@ -174,7 +217,7 @@ impl Config {
command
.stdout(stdout)
.stderr(stderr)
- .args(args)
+ .args(args.clone())
.env("GRAPH_STORE_WRITE_BATCH_DURATION", "5")
.env("ETHEREUM_REORG_THRESHOLD", "0");
@@ -254,7 +297,7 @@ impl Default for Config {
port: 3021,
host: "localhost".to_string(),
},
- graph_node: GraphNodeConfig::default(),
+ graph_node: GraphNodeConfig::from_env(),
graph_cli,
num_parallel_tests,
timeout: Duration::from_secs(600),
diff --git a/tests/src/subgraph.rs b/tests/src/subgraph.rs
index 92e42836b68..4bd4a17f9ad 100644
--- a/tests/src/subgraph.rs
+++ b/tests/src/subgraph.rs
@@ -26,7 +26,7 @@ pub struct Subgraph {
}
impl Subgraph {
- fn dir(name: &str) -> TestFile {
+ pub fn dir(name: &str) -> TestFile {
TestFile::new(&format!("integration-tests/{name}"))
}
@@ -47,8 +47,11 @@ impl Subgraph {
Ok(())
}
- /// Deploy the subgraph by running the required `graph` commands
- pub async fn deploy(name: &str, contracts: &[Contract]) -> anyhow::Result {
+ /// Prepare the subgraph for deployment by patching contracts and checking for subgraph datasources
+ pub async fn prepare(
+ name: &str,
+ contracts: &[Contract],
+ ) -> anyhow::Result<(TestFile, String, bool)> {
let dir = Self::dir(name);
let name = format!("test/{name}");
@@ -62,6 +65,13 @@ impl Subgraph {
.and_then(|ds| ds.iter().find(|d| d["kind"].as_str() == Some("subgraph")))
.is_some();
+ Ok((dir, name, has_subgraph_datasource))
+ }
+
+ /// Deploy the subgraph by running the required `graph` commands
+ pub async fn deploy(name: &str, contracts: &[Contract]) -> anyhow::Result {
+ let (dir, name, has_subgraph_datasource) = Self::prepare(name, contracts).await?;
+
// graph codegen subgraph.yaml
let mut prog = Command::new(&CONFIG.graph_cli);
let mut cmd = prog.arg("codegen").arg("subgraph.yaml.patched");
diff --git a/tests/tests/gnd_tests.rs b/tests/tests/gnd_tests.rs
new file mode 100644
index 00000000000..315521be357
--- /dev/null
+++ b/tests/tests/gnd_tests.rs
@@ -0,0 +1,148 @@
+use anyhow::anyhow;
+use graph::futures03::StreamExt;
+use graph_tests::config::set_dev_mode;
+use graph_tests::contract::Contract;
+use graph_tests::subgraph::Subgraph;
+use graph_tests::{error, status, CONFIG};
+
+mod integration_tests;
+
+use integration_tests::{
+ stop_graph_node, subgraph_data_sources, test_block_handlers,
+ test_multiple_subgraph_datasources, yarn_workspace, TestCase, TestResult,
+};
+
+/// The main test entrypoint.
+#[tokio::test]
+async fn gnd_tests() -> anyhow::Result<()> {
+ set_dev_mode(true);
+
+ let test_name_to_run = std::env::var("TEST_CASE").ok();
+
+ let cases = vec![
+ TestCase::new("block-handlers", test_block_handlers),
+ TestCase::new_with_source_subgraphs(
+ "subgraph-data-sources",
+ subgraph_data_sources,
+ vec!["QmWi3H11QFE2PiWx6WcQkZYZdA5UasaBptUJqGn54MFux5:source-subgraph"],
+ ),
+ TestCase::new_with_source_subgraphs(
+ "multiple-subgraph-datasources",
+ test_multiple_subgraph_datasources,
+ vec![
+ "QmYHp1bPEf7EoYBpEtJUpZv1uQHYQfWE4AhvR6frjB1Huj:source-subgraph-a",
+ "QmYBEzastJi7bsa722ac78tnZa6xNnV9vvweerY4kVyJtq:source-subgraph-b",
+ ],
+ ),
+ ];
+
+ // Filter the test cases if a specific test name is provided
+ let cases_to_run: Vec<_> = if let Some(test_name) = test_name_to_run {
+ cases
+ .into_iter()
+ .filter(|case| case.name == test_name)
+ .collect()
+ } else {
+ cases
+ };
+
+ let contracts = Contract::deploy_all().await?;
+
+ status!("setup", "Resetting database");
+ CONFIG.reset_database();
+
+ status!("setup", "Initializing yarn workspace");
+ yarn_workspace().await?;
+
+ for i in cases_to_run.iter() {
+ i.prepare(&contracts).await?;
+ }
+ status!("setup", "Prepared all cases");
+
+ let manifests = cases_to_run
+ .iter()
+ .map(|case| {
+ Subgraph::dir(&case.name)
+ .path
+ .join("subgraph.yaml")
+ .to_str()
+ .unwrap()
+ .to_string()
+ })
+ .collect::>()
+ .join(",");
+
+ let aliases = cases_to_run
+ .iter()
+ .filter_map(|case| case.source_subgraph.as_ref())
+ .flatten()
+ .filter_map(|source_subgraph| {
+ source_subgraph.alias().map(|alias| {
+ let manifest_path = Subgraph::dir(source_subgraph.test_name())
+ .path
+ .join("subgraph.yaml")
+ .to_str()
+ .unwrap()
+ .to_string();
+ format!("{}:{}", alias, manifest_path)
+ })
+ })
+ .collect::>();
+
+ let aliases_str = aliases.join(",");
+ let args = if aliases.is_empty() {
+ vec!["--manifests", &manifests]
+ } else {
+ vec!["--manifests", &manifests, "--sources", &aliases_str]
+ };
+
+ // Spawn graph-node.
+ status!("graph-node", "Starting graph-node");
+
+ let mut graph_node_child_command = CONFIG.spawn_graph_node_with_args(&args).await?;
+
+ let num_sources = aliases.len();
+
+ let stream = tokio_stream::iter(cases_to_run)
+ .enumerate()
+ .map(|(index, case)| {
+ let subgraph_name = format!("subgraph-{}", num_sources + index);
+ case.check_health_and_test(&contracts, subgraph_name)
+ })
+ .buffered(CONFIG.num_parallel_tests);
+
+ let mut results: Vec = stream.collect::>().await;
+ results.sort_by_key(|result| result.name.clone());
+
+ // Stop graph-node and read its output.
+ let graph_node_res = stop_graph_node(&mut graph_node_child_command).await;
+
+ status!(
+ "graph-node",
+ "graph-node logs are in {}",
+ CONFIG.graph_node.log_file.path.display()
+ );
+
+ match graph_node_res {
+ Ok(_) => {
+ status!("graph-node", "Stopped graph-node");
+ }
+ Err(e) => {
+ error!("graph-node", "Failed to stop graph-node: {}", e);
+ }
+ }
+
+ println!("\n\n{:=<60}", "");
+ println!("Test results:");
+ println!("{:-<60}", "");
+ for result in &results {
+ result.print();
+ }
+ println!("\n");
+
+ if results.iter().any(|result| !result.success()) {
+ Err(anyhow!("Some tests failed"))
+ } else {
+ Ok(())
+ }
+}
diff --git a/tests/tests/integration_tests.rs b/tests/tests/integration_tests.rs
index 9df36f7145a..be1465c0513 100644
--- a/tests/tests/integration_tests.rs
+++ b/tests/tests/integration_tests.rs
@@ -33,25 +33,25 @@ type TestFn = Box<
+ Send,
>;
-struct TestContext {
- subgraph: Subgraph,
- contracts: Vec,
+pub struct TestContext {
+ pub subgraph: Subgraph,
+ pub contracts: Vec,
}
-enum TestStatus {
+pub enum TestStatus {
Ok,
Err(anyhow::Error),
Panic(JoinError),
}
-struct TestResult {
- name: String,
- subgraph: Option,
- status: TestStatus,
+pub struct TestResult {
+ pub name: String,
+ pub subgraph: Option,
+ pub status: TestStatus,
}
impl TestResult {
- fn success(&self) -> bool {
+ pub fn success(&self) -> bool {
match self.status {
TestStatus::Ok => true,
_ => false,
@@ -64,7 +64,7 @@ impl TestResult {
}
}
- fn print(&self) {
+ pub fn print(&self) {
// ANSI escape sequences; see the comment in macros.rs about better colorization
const GREEN: &str = "\x1b[1;32m";
const RED: &str = "\x1b[1;31m";
@@ -94,14 +94,44 @@ impl TestResult {
}
}
-struct TestCase {
- name: String,
- test: TestFn,
- source_subgraph: Option,
+#[derive(Debug, Clone)]
+pub enum SourceSubgraph {
+ Subgraph(String),
+ WithAlias((String, String)), // (alias, test_name)
+}
+
+impl SourceSubgraph {
+ pub fn from_str(s: &str) -> Self {
+ if let Some((alias, subgraph)) = s.split_once(':') {
+ Self::WithAlias((alias.to_string(), subgraph.to_string()))
+ } else {
+ Self::Subgraph(s.to_string())
+ }
+ }
+
+ pub fn test_name(&self) -> &str {
+ match self {
+ Self::Subgraph(name) => name,
+ Self::WithAlias((_, name)) => name,
+ }
+ }
+
+ pub fn alias(&self) -> Option<&str> {
+ match self {
+ Self::Subgraph(_) => None,
+ Self::WithAlias((alias, _)) => Some(alias),
+ }
+ }
+}
+
+pub struct TestCase {
+ pub name: String,
+ pub test: TestFn,
+ pub source_subgraph: Option>,
}
impl TestCase {
- fn new(name: &str, test: fn(TestContext) -> T) -> Self
+ pub fn new(name: &str, test: fn(TestContext) -> T) -> Self
where
T: Future