Skip to content

Commit b0b65ef

Browse files
committed
test(multiversion-tests): add E2E, upgrade/rollback tests and CI integration
1 parent fd7698b commit b0b65ef

File tree

19 files changed

+1314
-445
lines changed

19 files changed

+1314
-445
lines changed

.github/workflows/multiversion.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Multiversion Tests
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "0 4 * * 1" # Monday 04:00 UTC
7+
8+
concurrency:
9+
group: multiversion-${{ github.ref }}
10+
cancel-in-progress: true
11+
12+
env:
13+
CARGO_TERM_COLOR: always
14+
RUST_BACKTRACE: 1
15+
16+
jobs:
17+
multiversion-tests:
18+
name: Cross-version integration tests
19+
runs-on: ubuntu-latest-16-cores
20+
timeout-minutes: 60
21+
22+
steps:
23+
- uses: actions/checkout@v4
24+
with:
25+
fetch-depth: 0
26+
27+
- name: Install Rust toolchain
28+
uses: dtolnay/rust-toolchain@stable
29+
with:
30+
toolchain: "1.93.0"
31+
32+
- name: Install system dependencies
33+
run: |
34+
sudo apt-get update
35+
sudo apt-get install -y clang libgmp-dev pkg-config libssl-dev
36+
37+
- uses: Swatinem/rust-cache@v2
38+
with:
39+
workspaces: |
40+
. -> target
41+
multiversion-tests -> multiversion-tests/target
42+
43+
- name: Run multiversion tests
44+
working-directory: multiversion-tests
45+
run: cargo test --test '*' -- --ignored --test-threads=1 --nocapture

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/target
2-
**/target
2+
multiversion-tests/target
33
.reth
44
**/.DS_Store
55
./database

multiversion-tests/Cargo.lock

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

multiversion-tests/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,15 @@ rust-version = "1.93.0"
88
publish = false
99

1010
[dependencies]
11-
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "signal", "time", "fs", "io-util"] }
11+
tokio = { version = "1", features = ["rt-multi-thread", "macros", "process", "signal", "time", "fs", "io-util", "sync"] }
1212
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
1313
serde = { version = "1", features = ["derive"] }
1414
serde_json = "1"
1515
toml = "0.8"
1616
tempfile = "3"
1717
tracing = "0.1"
18-
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
1918
thiserror = "2"
20-
nix = { version = "0.29", features = ["signal", "process", "mman"] }
19+
nix = { version = "0.29", features = ["signal", "process"] }
2120

2221
[dev-dependencies]
2322
test-log = { version = "0.2", features = ["trace"] }

multiversion-tests/claude/prompts.md

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 104 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,104 @@
1-
# Baseline config template for multiversion tests
2-
# Fields are patched by config::generate_config()
3-
api_port = 0
4-
gossip_port = 0
5-
base_directory = ""
6-
mining_key = ""
7-
mining_address = ""
8-
peers = []
9-
is_genesis = false
1+
node_mode = "Peer"
2+
sync_mode = "Full"
3+
mining_key = "0000000000000000000000000000000000000000000000000000000000000001"
4+
reward_address = "0x0000000000000000000000000000000000000001"
5+
base_directory = "/tmp/irys-test"
6+
consensus = "Testing"
7+
trusted_peers = []
8+
9+
[network_defaults]
10+
public_ip = "127.0.0.1"
11+
bind_ip = "127.0.0.1"
12+
13+
[gossip]
14+
public_port = 0
15+
bind_port = 0
16+
public_ip = "127.0.0.1"
17+
bind_ip = "127.0.0.1"
18+
19+
[http]
20+
public_port = 0
21+
bind_port = 0
22+
public_ip = "127.0.0.1"
23+
bind_ip = "127.0.0.1"
24+
25+
[reth.network]
26+
public_port = 0
27+
bind_port = 0
28+
use_random_ports = true
29+
public_ip = "127.0.0.1"
30+
bind_ip = "127.0.0.1"
31+
32+
[storage]
33+
num_writes_before_sync = 100
34+
35+
[data_sync]
36+
max_pending_chunk_requests = 1000
37+
max_storage_throughput_bps = 209715200
38+
bandwidth_adjustment_interval = "5s"
39+
chunk_request_timeout = "10s"
40+
41+
[packing.local]
42+
cpu_packing_concurrency = 2
43+
gpu_packing_batch_size = 0
44+
45+
[cache]
46+
cache_clean_lag = 0
47+
max_cache_size_bytes = 10737418240
48+
prune_at_capacity_percent = 80.0
49+
50+
[vdf]
51+
parallel_verification_thread_limit = 4
52+
53+
[mempool]
54+
max_pending_pledge_items = 1000
55+
max_pledges_per_item = 10
56+
max_pending_chunk_items = 500
57+
max_chunks_per_item = 100
58+
max_preheader_chunks_per_item = 50
59+
max_preheader_data_path_bytes = 4096
60+
max_valid_items = 10000
61+
max_invalid_items = 5000
62+
max_valid_chunks = 5000
63+
max_valid_submit_txs = 3000
64+
max_valid_commitment_addresses = 1000
65+
max_commitments_per_address = 5
66+
max_concurrent_mempool_tasks = 30
67+
max_concurrent_chunk_ingress_tasks = 30
68+
chunk_writer_buffer_size = 4096
69+
70+
[[oracles]]
71+
type = "mock"
72+
initial_price = 1.0
73+
incremental_change = 0.00000000000001
74+
smoothing_interval = 15
75+
initial_direction_up = true
76+
poll_interval_ms = 10000
77+
78+
[p2p_handshake]
79+
max_concurrent_handshakes = 32
80+
max_peers_per_response = 25
81+
max_retries = 8
82+
backoff_base_secs = 1
83+
backoff_cap_secs = 60
84+
blocklist_ttl_secs = 600
85+
server_peer_list_cap = 25
86+
87+
[p2p_gossip]
88+
broadcast_batch_size = 50
89+
broadcast_batch_throttle_interval = 100
90+
enable_scoring = true
91+
max_concurrent_gossip_chunks = 50
92+
93+
[p2p_pull]
94+
top_active_window = 10
95+
sample_size = 5
96+
max_attempts = 5
97+
98+
[sync]
99+
block_batch_size = 50
100+
periodic_sync_check_interval_secs = 30
101+
retry_block_request_timeout_secs = 30
102+
enable_periodic_sync_check = true
103+
wait_queue_slot_timeout_secs = 30
104+
wait_queue_slot_max_attempts = 3

multiversion-tests/src/binary.rs

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::fs::File;
12
use std::path::{Path, PathBuf};
23
use std::process::Stdio;
34

@@ -72,6 +73,11 @@ impl BinaryResolver {
7273
return Ok(resolved("new", rev, cached));
7374
}
7475

76+
let _lock = self.acquire_build_lock(&rev).await?;
77+
if cached.exists() {
78+
return Ok(resolved("new", rev, cached));
79+
}
80+
7581
self.build_at(&self.repo_root, &cached).await?;
7682
Ok(resolved("new", rev, cached))
7783
}
@@ -89,11 +95,19 @@ impl BinaryResolver {
8995
return Ok(resolved(git_ref, rev, cached));
9096
}
9197

98+
let _lock = self.acquire_build_lock(&rev).await?;
99+
if cached.exists() {
100+
return Ok(resolved(git_ref, rev, cached));
101+
}
102+
92103
let worktree_path = self.cache_dir.join(format!("worktree-{rev}"));
104+
self.prune_stale_worktrees().await;
93105
self.create_worktree(git_ref, &worktree_path).await?;
94-
let result = self.build_at(&worktree_path, &cached).await;
95-
self.remove_worktree(&worktree_path).await?;
96-
result?;
106+
let build_result = self.build_at(&worktree_path, &cached).await;
107+
if let Err(e) = self.remove_worktree(&worktree_path).await {
108+
tracing::warn!(error = %e, path = %worktree_path.display(), "failed to remove worktree");
109+
}
110+
build_result?;
97111

98112
Ok(resolved(git_ref, rev, cached))
99113
}
@@ -110,14 +124,29 @@ impl BinaryResolver {
110124
if !path.exists() {
111125
return Err(BinaryError::NotFound { path });
112126
}
113-
let rev = self.git_rev("HEAD").await?;
114-
Ok(Some(resolved(label, rev, path)))
127+
Ok(Some(resolved(label, "env-override".into(), path)))
115128
}
116129

117130
fn cached_binary_path(&self, rev: &str) -> PathBuf {
118131
self.cache_dir.join(format!("irys-{rev}"))
119132
}
120133

134+
/// Inter-process exclusive lock for a specific revision build.
135+
/// Prevents parallel test processes from creating the same worktree simultaneously.
136+
/// The lock is released when the returned File is dropped.
137+
async fn acquire_build_lock(&self, rev: &str) -> Result<File, BinaryError> {
138+
tokio::fs::create_dir_all(&self.cache_dir).await?;
139+
let lock_path = self.cache_dir.join(format!("build-{rev}.lock"));
140+
tokio::task::spawn_blocking(move || -> Result<File, std::io::Error> {
141+
let file = File::create(lock_path)?;
142+
file.lock()?;
143+
Ok(file)
144+
})
145+
.await
146+
.map_err(|e| BinaryError::Io(std::io::Error::other(e)))?
147+
.map_err(BinaryError::Io)
148+
}
149+
121150
async fn git_rev(&self, git_ref: &str) -> Result<String, BinaryError> {
122151
validate_git_ref(git_ref)?;
123152
self.git_rev_at(&self.repo_root, git_ref).await
@@ -127,7 +156,7 @@ impl BinaryResolver {
127156
validate_git_ref(git_ref)?;
128157

129158
let output = Command::new("git")
130-
.args(["rev-parse", "--short=12", "--"])
159+
.args(["rev-parse", "--short=12"])
131160
.arg(git_ref)
132161
.current_dir(dir)
133162
.stdout(Stdio::piped())
@@ -172,16 +201,37 @@ impl BinaryResolver {
172201
tokio::fs::create_dir_all(parent).await?;
173202
}
174203

204+
let built = match self
205+
.try_cargo_build(work_dir, &["--profile", "debug-release"], "debug-release")
206+
.await
207+
{
208+
Ok(path) => path,
209+
Err(BinaryError::BuildFailed { ref stderr, .. })
210+
if stderr.contains("is not defined") =>
211+
{
212+
tracing::info!("debug-release profile unavailable, falling back to --release");
213+
self.try_cargo_build(work_dir, &["--release"], "release")
214+
.await?
215+
}
216+
Err(e) => return Err(e),
217+
};
218+
219+
tokio::fs::copy(&built, output_path).await?;
220+
Ok(())
221+
}
222+
223+
async fn try_cargo_build(
224+
&self,
225+
work_dir: &Path,
226+
profile_args: &[&str],
227+
target_dir_name: &str,
228+
) -> Result<PathBuf, BinaryError> {
229+
let mut args = vec!["build"];
230+
args.extend(profile_args);
231+
args.extend(["--bin", "irys", "-p", "irys-chain"]);
232+
175233
let output = Command::new("cargo")
176-
.args([
177-
"build",
178-
"--profile",
179-
"debug-release",
180-
"--bin",
181-
"irys",
182-
"-p",
183-
"irys-chain",
184-
])
234+
.args(&args)
185235
.current_dir(work_dir)
186236
.stdout(Stdio::piped())
187237
.stderr(Stdio::piped())
@@ -196,13 +246,24 @@ impl BinaryResolver {
196246
});
197247
}
198248

199-
let built = work_dir.join("target/debug-release/irys");
249+
let built = work_dir.join(format!("target/{target_dir_name}/irys"));
200250
if !built.exists() {
201251
return Err(BinaryError::NotFound { path: built });
202252
}
203-
tokio::fs::copy(&built, output_path).await?;
253+
Ok(built)
254+
}
204255

205-
Ok(())
256+
async fn prune_stale_worktrees(&self) {
257+
let result = Command::new("git")
258+
.args(["worktree", "prune"])
259+
.current_dir(&self.repo_root)
260+
.stdout(Stdio::null())
261+
.stderr(Stdio::null())
262+
.status()
263+
.await;
264+
if let Err(e) = result {
265+
tracing::warn!(error = %e, "git worktree prune failed");
266+
}
206267
}
207268

208269
async fn remove_worktree(&self, path: &Path) -> Result<(), BinaryError> {
@@ -211,12 +272,19 @@ impl BinaryResolver {
211272
.arg(path)
212273
.current_dir(&self.repo_root)
213274
.stdout(Stdio::null())
214-
.stderr(Stdio::null())
275+
.stderr(Stdio::piped())
215276
.output()
216277
.await;
217278

218-
if let Err(e) = output {
219-
tracing::warn!(path = %path.display(), error = %e, "failed to remove worktree");
279+
match output {
280+
Ok(o) if !o.status.success() => {
281+
let stderr = String::from_utf8_lossy(&o.stderr);
282+
tracing::warn!(path = %path.display(), stderr = %stderr, "worktree removal failed");
283+
}
284+
Err(e) => {
285+
tracing::warn!(path = %path.display(), error = %e, "failed to remove worktree");
286+
}
287+
_ => {}
220288
}
221289

222290
Ok(())

0 commit comments

Comments
 (0)