-
Notifications
You must be signed in to change notification settings - Fork 47
Expand file tree
/
Copy pathdocker.rs
More file actions
186 lines (157 loc) 路 6.12 KB
/
Copy pathdocker.rs
File metadata and controls
186 lines (157 loc) 路 6.12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// Copyright 2022-2024 Protocol Labs
// SPDX-License-Identifier: Apache-2.0, MIT
//! Utility methods and entry point for tests using the docker materializer.
//!
//! # Example
//!
//! `cargo test -p fendermint_materializer --test docker -- --nocapture`
use std::{
collections::BTreeSet,
env::current_dir,
path::PathBuf,
pin::Pin,
time::{Duration, Instant},
};
use anyhow::{anyhow, Context};
use ethers::providers::Middleware;
use fendermint_materializer::{
docker::{DockerMaterializer, DockerMaterials},
manifest::Manifest,
testnet::Testnet,
HasCometBftApi, HasEthApi, TestnetName,
};
use futures::Future;
use lazy_static::lazy_static;
use tendermint_rpc::Client;
pub type DockerTestnet = Testnet<DockerMaterials, DockerMaterializer>;
lazy_static! {
static ref CI_PROFILE: bool = std::env::var("PROFILE").unwrap_or_default() == "ci";
static ref STARTUP_TIMEOUT: Duration = Duration::from_secs(60);
static ref TEARDOWN_TIMEOUT: Duration = Duration::from_secs(30);
static ref PRINT_LOGS_ON_ERROR: bool = *CI_PROFILE;
}
/// Want to keep the testnet artifacts in the `tests/testnets` directory.
fn tests_dir() -> PathBuf {
let dir = current_dir().unwrap();
debug_assert!(
dir.ends_with("materializer"),
"expected the current directory to be the crate"
);
dir.join("tests")
}
/// Directory where we keep the docker-materializer related data files.
fn test_data_dir() -> PathBuf {
tests_dir().join("docker-materializer-data")
}
/// Parse a manifest from the `tests/manifests` directory.
fn read_manifest(file_name: &str) -> anyhow::Result<Manifest> {
let manifest = tests_dir().join("manifests").join(file_name);
let manifest = Manifest::from_file(&manifest)?;
Ok(manifest)
}
/// Parse a manifest file in the `manifests` directory, clean up any corresponding
/// testnet resources, then materialize a testnet and run some tests.
pub async fn with_testnet<F, G>(manifest_file_name: &str, alter: G, test: F) -> anyhow::Result<()>
where
// https://users.rust-lang.org/t/function-that-takes-a-closure-with-mutable-reference-that-returns-a-future/54324
F: for<'a> FnOnce(
&Manifest,
&mut DockerMaterializer,
&'a mut DockerTestnet,
) -> Pin<Box<dyn Future<Output = anyhow::Result<()>> + 'a>>,
G: FnOnce(&mut Manifest),
{
let testnet_name = TestnetName::new(
PathBuf::from(manifest_file_name)
.file_stem()
.expect("filename missing")
.to_string_lossy()
.to_string(),
);
let mut manifest = read_manifest(manifest_file_name)?;
// Make any test-specific modifications to the manifest if that makes sense.
alter(&mut manifest);
// Make sure it's a sound manifest.
manifest
.validate(&testnet_name)
.await
.context("failed to validate manifest")?;
// NOTE: Add `with_policy(DropPolicy::PERSISTENT)` if you want containers to stick around for inspection,
// but logs and env vars should be available on disk even if the testnet is torn down at the end.
let mut materializer = DockerMaterializer::new(&test_data_dir(), 0)?;
// make sure we start with clean slate by removing any previous files
materializer
.remove(&testnet_name)
.await
.context("failed to remove testnet")?;
let mut testnet = Testnet::setup(&mut materializer, &testnet_name, &manifest)
.await
.context("failed to set up testnet")?;
let started = wait_for_startup(&testnet).await?;
let res = if started {
test(&manifest, &mut materializer, &mut testnet).await
} else {
Err(anyhow!("the startup sequence timed out"))
};
// Print all logs on failure.
// Some might be available in logs in the files which are left behind,
// e.g. for `fendermint` we have logs, but maybe not for `cometbft`.
if res.is_err() && *PRINT_LOGS_ON_ERROR {
for (name, node) in testnet.nodes() {
let name = name.path_string();
for log in node.fendermint_logs().await {
eprintln!("{name}/fendermint: {log}");
}
for log in node.cometbft_logs().await {
eprintln!("{name}/cometbft: {log}");
}
for log in node.ethapi_logs().await {
eprintln!("{name}/ethapi: {log}");
}
}
}
// Tear down the testnet.
drop(testnet);
// Allow some time for containers to be dropped.
// This only happens if the testnet setup succeeded,
// otherwise the system shuts down too quick, but
// at least we can inspect the containers.
// If they don't all get dropped, `docker system prune` helps.
let drop_handle = materializer.take_dropper();
let _ = tokio::time::timeout(*TEARDOWN_TIMEOUT, drop_handle).await;
res
}
/// Allow time for things to consolidate and APIs to start.
async fn wait_for_startup(testnet: &DockerTestnet) -> anyhow::Result<bool> {
let start = Instant::now();
let mut started = BTreeSet::new();
'startup: loop {
if start.elapsed() > *STARTUP_TIMEOUT {
return Ok(false);
}
tokio::time::sleep(Duration::from_secs(5)).await;
for (name, dnode) in testnet.nodes() {
if started.contains(name) {
continue;
}
let client = dnode.cometbft_http_provider()?;
if let Err(e) = client.abci_info().await {
eprintln!("CometBFT on {name} still fails: {e}");
continue 'startup;
}
if let Some(client) = dnode.ethapi_http_provider()? {
if let Err(e) = client.get_chainid().await {
eprintln!("EthAPI on {name} still fails: {e}");
continue 'startup;
}
}
eprintln!("APIs on {name} started");
started.insert(name.clone());
}
// All of them succeeded.
return Ok(true);
}
}
// Run these tests serially because they share a common `materializer-state.json` file with the port mappings.
// Unfortunately the `#[serial]` macro can only be applied to module blocks, not this.
mod docker_tests;