1use crate::{
2 binary::FreenetBinary,
3 docker::{DockerNatBackend, DockerNatConfig},
4 network::TestNetwork,
5 peer::{get_free_port, TestPeer},
6 process::{self, PeerProcess},
7 remote::{PeerLocation, RemoteMachine},
8 Error, Result,
9};
10use chrono::Utc;
11use std::collections::HashMap;
12use std::fs;
13use std::net::Ipv4Addr;
14use std::path::{Path, PathBuf};
15use std::process::Command;
16use std::time::{Duration, SystemTime};
17
18#[derive(Debug, Clone)]
20pub enum Backend {
21 Local,
23 DockerNat(DockerNatConfig),
25}
26
27impl Default for Backend {
28 fn default() -> Self {
29 if std::env::var("FREENET_TEST_DOCKER_NAT").is_ok() {
31 Backend::DockerNat(DockerNatConfig::default())
32 } else {
33 Backend::Local
34 }
35 }
36}
37
38struct GatewayInfo {
39 address: String,
40 public_key_path: PathBuf,
41}
42
43pub struct NetworkBuilder {
45 gateways: usize,
46 peers: usize,
47 binary: FreenetBinary,
48 min_connectivity: f64,
49 connectivity_timeout: Duration,
50 preserve_data_on_failure: bool,
51 preserve_data_on_success: bool,
52 peer_locations: HashMap<usize, PeerLocation>,
53 default_location: PeerLocation,
54 min_connections: Option<usize>,
55 max_connections: Option<usize>,
56 start_stagger: Duration,
57 backend: Backend,
58}
59
60impl Default for NetworkBuilder {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl NetworkBuilder {
67 pub fn new() -> Self {
68 Self {
69 gateways: 1,
70 peers: 3,
71 binary: FreenetBinary::default(),
72 min_connectivity: 1.0, connectivity_timeout: Duration::from_secs(30),
74 preserve_data_on_failure: false,
75 preserve_data_on_success: false,
76 peer_locations: HashMap::new(),
77 default_location: PeerLocation::Local,
78 min_connections: None,
79 max_connections: None,
80 start_stagger: Duration::from_millis(500),
81 backend: Backend::default(),
82 }
83 }
84
85 pub fn gateways(mut self, n: usize) -> Self {
87 self.gateways = n;
88 self
89 }
90
91 pub fn peers(mut self, n: usize) -> Self {
93 self.peers = n;
94 self
95 }
96
97 pub fn binary(mut self, binary: FreenetBinary) -> Self {
99 self.binary = binary;
100 self
101 }
102
103 pub fn require_connectivity(mut self, ratio: f64) -> Self {
105 self.min_connectivity = ratio;
106 self
107 }
108
109 pub fn connectivity_timeout(mut self, timeout: Duration) -> Self {
111 self.connectivity_timeout = timeout;
112 self
113 }
114
115 pub fn min_connections(mut self, min: usize) -> Self {
117 self.min_connections = Some(min);
118 self
119 }
120
121 pub fn max_connections(mut self, max: usize) -> Self {
123 self.max_connections = Some(max);
124 self
125 }
126
127 pub fn start_stagger(mut self, delay: Duration) -> Self {
129 self.start_stagger = delay;
130 self
131 }
132
133 pub fn preserve_temp_dirs_on_failure(mut self, preserve: bool) -> Self {
135 self.preserve_data_on_failure = preserve;
136 self
137 }
138
139 pub fn preserve_temp_dirs_on_success(mut self, preserve: bool) -> Self {
141 self.preserve_data_on_success = preserve;
142 self
143 }
144
145 pub fn peer_location(mut self, index: usize, location: PeerLocation) -> Self {
148 self.peer_locations.insert(index, location);
149 self
150 }
151
152 pub fn default_location(mut self, location: PeerLocation) -> Self {
154 self.default_location = location;
155 self
156 }
157
158 pub fn distribute_across_remotes(mut self, machines: Vec<RemoteMachine>) -> Self {
161 let total_peers = self.gateways + self.peers;
162 for (idx, machine) in (0..total_peers).zip(machines.iter().cycle()) {
163 self.peer_locations
164 .insert(idx, PeerLocation::Remote(machine.clone()));
165 }
166 self
167 }
168
169 pub fn backend(mut self, backend: Backend) -> Self {
171 self.backend = backend;
172 self
173 }
174
175 pub async fn build(self) -> Result<TestNetwork> {
177 match self.backend.clone() {
178 Backend::Local => self.build_local().await,
179 Backend::DockerNat(config) => self.build_docker_nat(config).await,
180 }
181 }
182
183 async fn build_local(self) -> Result<TestNetwork> {
185 let binary_path = self.binary.resolve()?;
186
187 tracing::info!(
188 "Starting test network: {} gateways, {} peers",
189 self.gateways,
190 self.peers
191 );
192
193 let base_dir = resolve_base_dir();
194 fs::create_dir_all(&base_dir)?;
195 cleanup_old_runs(&base_dir, 5)?;
196 let run_root = create_run_directory(&base_dir)?;
197
198 let mut run_status = RunStatusGuard::new(&run_root);
199
200 let mut gateways = Vec::new();
202 for i in 0..self.gateways {
203 let peer = match self.start_peer(&binary_path, i, true, &run_root).await {
204 Ok(peer) => peer,
205 Err(err) => {
206 let detail = format!("failed to start gateway {i}: {err}");
207 run_status.mark("failure", Some(&detail));
208 return Err(err);
209 }
210 };
211 gateways.push(peer);
212 }
213
214 let gateway_info: Vec<_> = gateways
216 .iter()
217 .map(|gw| GatewayInfo {
218 address: format!("{}:{}", gw.network_address, gw.network_port),
219 public_key_path: gw
220 .public_key_path
221 .clone()
222 .expect("Gateway must have public key"),
223 })
224 .collect();
225
226 let mut peers = Vec::new();
228 for i in 0..self.peers {
229 let peer = match self
230 .start_peer_with_gateways(
231 &binary_path,
232 i + self.gateways,
233 false,
234 &gateway_info,
235 &run_root,
236 )
237 .await
238 {
239 Ok(peer) => peer,
240 Err(err) => {
241 let detail = format!("failed to start peer {}: {}", i + self.gateways, err);
242 run_status.mark("failure", Some(&detail));
243 return Err(err);
244 }
245 };
246 peers.push(peer);
247 if i + 1 < self.peers && !self.start_stagger.is_zero() {
248 tokio::time::sleep(self.start_stagger).await;
249 }
250 }
251
252 let network = TestNetwork::new(gateways, peers, self.min_connectivity, run_root.clone());
253
254 match network
256 .wait_until_ready_with_timeout(self.connectivity_timeout)
257 .await
258 {
259 Ok(()) => {
260 if self.preserve_data_on_success {
261 match preserve_network_state(&network) {
262 Ok(path) => {
263 println!("Network data directories preserved at {}", path.display());
264 }
265 Err(err) => {
266 eprintln!(
267 "Failed to preserve network data directories after success: {}",
268 err
269 );
270 }
271 }
272 }
273 let detail = format!("success: gateways={}, peers={}", self.gateways, self.peers);
274 run_status.mark("success", Some(&detail));
275 Ok(network)
276 }
277 Err(err) => {
278 if let Err(log_err) = dump_recent_logs(&network) {
279 eprintln!("Failed to dump logs after connectivity error: {}", log_err);
280 }
281 if self.preserve_data_on_failure {
282 match preserve_network_state(&network) {
283 Ok(path) => {
284 eprintln!("Network data directories preserved at {}", path.display());
285 }
286 Err(copy_err) => {
287 eprintln!("Failed to preserve network data directories: {}", copy_err);
288 }
289 }
290 }
291 let detail = err.to_string();
292 run_status.mark("failure", Some(&detail));
293 Err(err)
294 }
295 }
296 }
297
298 pub fn build_sync(self) -> Result<TestNetwork> {
300 tokio::runtime::Runtime::new()?.block_on(self.build())
301 }
302
303 async fn start_peer(
304 &self,
305 binary_path: &PathBuf,
306 index: usize,
307 is_gateway: bool,
308 run_root: &Path,
309 ) -> Result<TestPeer> {
310 self.start_peer_with_gateways(binary_path, index, is_gateway, &[], run_root)
311 .await
312 }
313
314 async fn start_peer_with_gateways(
315 &self,
316 binary_path: &PathBuf,
317 index: usize,
318 is_gateway: bool,
319 gateway_info: &[GatewayInfo],
320 run_root: &Path,
321 ) -> Result<TestPeer> {
322 let location = self
324 .peer_locations
325 .get(&index)
326 .cloned()
327 .unwrap_or_else(|| self.default_location.clone());
328
329 let id = if is_gateway {
330 format!("gw{}", index)
331 } else {
332 format!("peer{}", index)
333 };
334
335 let network_address = match &location {
337 PeerLocation::Local => {
338 let addr_index = index as u32;
339 let second_octet = ((addr_index / 256) % 254 + 1) as u8;
340 let third_octet = (addr_index % 256) as u8;
341 Ipv4Addr::new(127, second_octet, third_octet, 1).to_string()
342 }
343 PeerLocation::Remote(remote) => {
344 remote.discover_public_address()?
346 }
347 };
348
349 let (ws_port, network_port) = match &location {
352 PeerLocation::Local => (get_free_port()?, get_free_port()?),
353 PeerLocation::Remote(_) => (0, 0), };
355
356 let data_dir = create_peer_dir(run_root, &id)?;
357
358 tracing::debug!(
359 "Starting {} {} - ws:{} net:{}",
360 if is_gateway { "gateway" } else { "peer" },
361 id,
362 ws_port,
363 network_port
364 );
365
366 let keypair_path = data_dir.join("keypair.pem");
368 let public_key_path = data_dir.join("public_key.pem");
369 generate_keypair(&keypair_path, &public_key_path)?;
370
371 if let PeerLocation::Remote(remote) = &location {
374 let remote_data_dir = remote.remote_work_dir().join(&id);
375
376 let mkdir_cmd = format!("mkdir -p {}", remote_data_dir.display());
378 remote.exec(&mkdir_cmd)?;
379
380 let remote_keypair = remote_data_dir.join("keypair.pem");
382 let remote_pubkey = remote_data_dir.join("public_key.pem");
383 remote.scp_upload(&keypair_path, remote_keypair.to_str().unwrap())?;
384 remote.scp_upload(&public_key_path, remote_pubkey.to_str().unwrap())?;
385
386 if !is_gateway {
388 for gw in gateway_info {
389 let gw_pubkey_name = gw.public_key_path.file_name().ok_or_else(|| {
390 Error::PeerStartupFailed("Invalid gateway pubkey path".to_string())
391 })?;
392 let remote_gw_pubkey = remote_data_dir.join(gw_pubkey_name);
393 remote.scp_upload(&gw.public_key_path, remote_gw_pubkey.to_str().unwrap())?;
394 }
395 }
396 }
397
398 let mut args = vec![
400 "network".to_string(),
401 "--data-dir".to_string(),
402 match &location {
403 PeerLocation::Local => data_dir.to_string_lossy().to_string(),
404 PeerLocation::Remote(remote) => remote
405 .remote_work_dir()
406 .join(&id)
407 .to_string_lossy()
408 .to_string(),
409 },
410 "--config-dir".to_string(),
411 match &location {
412 PeerLocation::Local => data_dir.to_string_lossy().to_string(),
413 PeerLocation::Remote(remote) => remote
414 .remote_work_dir()
415 .join(&id)
416 .to_string_lossy()
417 .to_string(),
418 },
419 "--ws-api-port".to_string(),
420 ws_port.to_string(),
421 "--network-address".to_string(),
422 network_address.clone(),
423 "--network-port".to_string(),
424 network_port.to_string(),
425 "--public-network-address".to_string(),
426 network_address.clone(),
427 "--public-network-port".to_string(),
428 network_port.to_string(),
429 "--skip-load-from-network".to_string(),
430 ];
431
432 if is_gateway {
433 args.push("--is-gateway".to_string());
434 }
435
436 args.push("--transport-keypair".to_string());
437 let keypair_arg = match &location {
438 PeerLocation::Local => data_dir.join("keypair.pem").to_string_lossy().to_string(),
439 PeerLocation::Remote(remote) => remote
440 .remote_work_dir()
441 .join(&id)
442 .join("keypair.pem")
443 .to_string_lossy()
444 .to_string(),
445 };
446 args.push(keypair_arg);
447
448 if !is_gateway && !gateway_info.is_empty() {
450 let gateways_toml = data_dir.join("gateways.toml");
451 let mut content = String::new();
452 for gw in gateway_info {
453 let gw_pubkey_path = match &location {
454 PeerLocation::Local => gw.public_key_path.clone(),
455 PeerLocation::Remote(remote) => {
456 let gw_pubkey_name = gw.public_key_path.file_name().ok_or_else(|| {
457 Error::PeerStartupFailed("Invalid gateway pubkey path".to_string())
458 })?;
459 remote.remote_work_dir().join(&id).join(gw_pubkey_name)
460 }
461 };
462 content.push_str(&format!(
463 "[[gateways]]\n\
464 address = {{ hostname = \"{}\" }}\n\
465 public_key = \"{}\"\n\n",
466 gw.address,
467 gw_pubkey_path.display()
468 ));
469 }
470 std::fs::write(&gateways_toml, content)?;
471
472 if let PeerLocation::Remote(remote) = &location {
474 let remote_gateways_toml = remote.remote_work_dir().join(&id).join("gateways.toml");
475 remote.scp_upload(&gateways_toml, remote_gateways_toml.to_str().unwrap())?;
476 }
477 }
478
479 let env_vars = vec![
481 ("NETWORK_ADDRESS".to_string(), network_address.clone()),
482 (
483 "PUBLIC_NETWORK_ADDRESS".to_string(),
484 network_address.clone(),
485 ),
486 ("PUBLIC_NETWORK_PORT".to_string(), network_port.to_string()),
487 ];
488
489 if let Some(min_conn) = self.min_connections {
490 args.push("--min-number-of-connections".to_string());
491 args.push(min_conn.to_string());
492 }
493 if let Some(max_conn) = self.max_connections {
494 args.push("--max-number-of-connections".to_string());
495 args.push(max_conn.to_string());
496 }
497
498 let process: Box<dyn PeerProcess + Send> = match &location {
500 PeerLocation::Local => Box::new(process::spawn_local_peer(
501 binary_path,
502 &args,
503 &data_dir,
504 &env_vars,
505 )?),
506 PeerLocation::Remote(remote) => {
507 let remote_data_dir = remote.remote_work_dir().join(&id);
508 let local_cache_dir = run_root.join(format!("{}-cache", id));
509 std::fs::create_dir_all(&local_cache_dir)?;
510
511 Box::new(
512 process::spawn_remote_peer(
513 binary_path,
514 &args,
515 remote,
516 &remote_data_dir,
517 &local_cache_dir,
518 &env_vars,
519 )
520 .await?,
521 )
522 }
523 };
524
525 tokio::time::sleep(Duration::from_millis(100)).await;
527
528 Ok(TestPeer {
529 id,
530 is_gateway,
531 ws_port,
532 network_port,
533 network_address,
534 data_dir,
535 process,
536 public_key_path: Some(public_key_path),
537 location,
538 })
539 }
540
541 async fn build_docker_nat(self, config: DockerNatConfig) -> Result<TestNetwork> {
543 let binary_path = self.binary.resolve()?;
544
545 tracing::info!(
546 "Starting Docker NAT test network: {} gateways, {} peers",
547 self.gateways,
548 self.peers
549 );
550
551 let base_dir = resolve_base_dir();
552 fs::create_dir_all(&base_dir)?;
553 cleanup_old_runs(&base_dir, 5)?;
554 let run_root = create_run_directory(&base_dir)?;
555
556 let mut run_status = RunStatusGuard::new(&run_root);
557
558 let mut docker_backend = DockerNatBackend::new(config).await.map_err(|e| {
560 run_status.mark("failure", Some(&format!("Docker init failed: {}", e)));
561 e
562 })?;
563
564 docker_backend.create_public_network().await.map_err(|e| {
566 run_status.mark("failure", Some(&format!("Failed to create public network: {}", e)));
567 e
568 })?;
569
570 let ws_port: u16 = 9000;
572 let network_port: u16 = 31337;
573
574 let mut gateways = Vec::new();
576 for i in 0..self.gateways {
577 let data_dir = create_peer_dir(&run_root, &format!("gw{}", i))?;
578
579 let keypair_path = data_dir.join("keypair.pem");
581 let public_key_path = data_dir.join("public_key.pem");
582 generate_keypair(&keypair_path, &public_key_path)?;
583
584 let (info, process) = docker_backend
585 .create_gateway(
586 i,
587 &binary_path,
588 &keypair_path,
589 &public_key_path,
590 ws_port,
591 network_port,
592 &run_root,
593 )
594 .await
595 .map_err(|e| {
596 let detail = format!("failed to start gateway {}: {}", i, e);
597 run_status.mark("failure", Some(&detail));
598 e
599 })?;
600
601 let peer = TestPeer {
602 id: format!("gw{}", i),
603 is_gateway: true,
604 ws_port: info.host_ws_port,
605 network_port: info.network_port,
606 network_address: info.public_ip.to_string(),
607 data_dir,
608 process: Box::new(process),
609 public_key_path: Some(public_key_path),
610 location: PeerLocation::Local, };
612 gateways.push(peer);
613 }
614
615 let gateway_info: Vec<_> = gateways
617 .iter()
618 .map(|gw| GatewayInfo {
619 address: format!("{}:{}", gw.network_address, network_port),
620 public_key_path: gw
621 .public_key_path
622 .clone()
623 .expect("Gateway must have public key"),
624 })
625 .collect();
626
627 let mut peers = Vec::new();
629 for i in 0..self.peers {
630 let peer_index = i + self.gateways;
631 let data_dir = create_peer_dir(&run_root, &format!("peer{}", peer_index))?;
632
633 let keypair_path = data_dir.join("keypair.pem");
635 let public_key_path = data_dir.join("public_key.pem");
636 generate_keypair(&keypair_path, &public_key_path)?;
637
638 let gateways_toml_path = data_dir.join("gateways.toml");
640 let mut gateways_content = String::new();
641 for gw in &gateway_info {
642 gateways_content.push_str(&format!(
643 "[[gateways]]\n\
644 address = {{ hostname = \"{}\" }}\n\
645 public_key = \"/config/gw_public_key.pem\"\n\n",
646 gw.address,
647 ));
648 }
649 std::fs::write(&gateways_toml_path, &gateways_content)?;
650
651 let gateway_public_key_path = gateway_info.first().map(|gw| gw.public_key_path.clone());
653
654 let (info, process) = docker_backend
655 .create_peer(
656 peer_index,
657 &binary_path,
658 &keypair_path,
659 &public_key_path,
660 &gateways_toml_path,
661 gateway_public_key_path.as_deref(),
662 ws_port,
663 network_port,
664 &run_root,
665 )
666 .await
667 .map_err(|e| {
668 let detail = format!("failed to start peer {}: {}", peer_index, e);
669 run_status.mark("failure", Some(&detail));
670 e
671 })?;
672
673 let peer = TestPeer {
674 id: format!("peer{}", peer_index),
675 is_gateway: false,
676 ws_port: info.host_ws_port,
677 network_port: info.network_port,
678 network_address: info.private_ip.to_string(),
679 data_dir,
680 process: Box::new(process),
681 public_key_path: Some(public_key_path),
682 location: PeerLocation::Local,
683 };
684 peers.push(peer);
685
686 if i + 1 < self.peers && !self.start_stagger.is_zero() {
687 tokio::time::sleep(self.start_stagger).await;
688 }
689 }
690
691 let network = TestNetwork::new_with_docker(
693 gateways,
694 peers,
695 self.min_connectivity,
696 run_root.clone(),
697 Some(docker_backend),
698 );
699
700 match network
702 .wait_until_ready_with_timeout(self.connectivity_timeout)
703 .await
704 {
705 Ok(()) => {
706 if self.preserve_data_on_success {
707 println!("Network data directories preserved at {}", run_root.display());
708 }
709 let detail = format!("success: gateways={}, peers={} (Docker NAT)", self.gateways, self.peers);
710 run_status.mark("success", Some(&detail));
711 Ok(network)
712 }
713 Err(err) => {
714 if let Err(log_err) = dump_recent_logs(&network) {
715 eprintln!("Failed to dump logs after connectivity error: {}", log_err);
716 }
717 if self.preserve_data_on_failure {
718 eprintln!("Network data directories preserved at {}", run_root.display());
719 }
720 let detail = err.to_string();
721 run_status.mark("failure", Some(&detail));
722 Err(err)
723 }
724 }
725 }
726}
727
728fn resolve_base_dir() -> PathBuf {
729 if let Some(path) = std::env::var_os("FREENET_TEST_NETWORK_BASE_DIR") {
730 PathBuf::from(path)
731 } else if let Ok(home) = std::env::var("HOME") {
732 PathBuf::from(home).join("code/tmp/freenet-test-networks")
733 } else {
734 std::env::temp_dir().join("freenet-test-networks")
735 }
736}
737
738fn cleanup_old_runs(base_dir: &Path, max_runs: usize) -> Result<()> {
739 let mut runs: Vec<(PathBuf, SystemTime)> = fs::read_dir(base_dir)?
740 .filter_map(|entry| {
741 let entry = entry.ok()?;
742 let file_type = entry.file_type().ok()?;
743 if !file_type.is_dir() {
744 return None;
745 }
746 let metadata = entry.metadata().ok()?;
747 let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
748 Some((entry.path(), modified))
749 })
750 .collect();
751
752 if runs.len() <= max_runs {
753 return Ok(());
754 }
755
756 runs.sort_by_key(|(_, modified)| *modified);
757 let remove_count = runs.len() - max_runs;
758 for (path, _) in runs.into_iter().take(remove_count) {
759 if let Err(err) = fs::remove_dir_all(&path) {
760 tracing::warn!(
761 ?err,
762 path = %path.display(),
763 "Failed to remove old freenet test network run directory"
764 );
765 }
766 }
767
768 Ok(())
769}
770
771fn create_run_directory(base_dir: &Path) -> Result<PathBuf> {
772 let timestamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
773 for attempt in 0..100 {
774 let candidate = if attempt == 0 {
775 base_dir.join(×tamp)
776 } else {
777 base_dir.join(format!("{}-{}", ×tamp, attempt))
778 };
779 if !candidate.exists() {
780 fs::create_dir_all(&candidate)?;
781 return Ok(candidate);
782 }
783 }
784
785 Err(Error::Other(anyhow::anyhow!(
786 "Unable to allocate run directory after repeated attempts"
787 )))
788}
789
790fn create_peer_dir(run_root: &Path, id: &str) -> Result<PathBuf> {
791 let dir = run_root.join(id);
792 fs::create_dir_all(&dir)?;
793 Ok(dir)
794}
795
796struct RunStatusGuard {
797 status_path: PathBuf,
798}
799
800impl RunStatusGuard {
801 fn new(run_root: &Path) -> Self {
802 let status_path = run_root.join("run_status.txt");
803 let _ = fs::write(&status_path, b"status=initializing\n");
804 Self { status_path }
805 }
806
807 fn mark(&mut self, status: &str, detail: Option<&str>) {
808 let mut content = format!("status={}", status);
809 if let Some(detail) = detail {
810 content.push('\n');
811 content.push_str("detail=");
812 content.push_str(detail);
813 }
814 content.push('\n');
815 if let Err(err) = fs::write(&self.status_path, content) {
816 tracing::warn!(
817 ?err,
818 path = %self.status_path.display(),
819 "Failed to write run status"
820 );
821 }
822 }
823}
824
825fn generate_keypair(
826 private_key_path: &std::path::Path,
827 public_key_path: &std::path::Path,
828) -> Result<()> {
829 let output = Command::new("openssl")
831 .args([
832 "genpkey",
833 "-algorithm",
834 "RSA",
835 "-out",
836 private_key_path.to_str().unwrap(),
837 "-pkeyopt",
838 "rsa_keygen_bits:2048",
839 ])
840 .output()?;
841
842 if !output.status.success() {
843 return Err(Error::Other(anyhow::anyhow!(
844 "Failed to generate private key: {}",
845 String::from_utf8_lossy(&output.stderr)
846 )));
847 }
848
849 let output = Command::new("openssl")
851 .args([
852 "rsa",
853 "-pubout",
854 "-in",
855 private_key_path.to_str().unwrap(),
856 "-out",
857 public_key_path.to_str().unwrap(),
858 ])
859 .output()?;
860
861 if !output.status.success() {
862 return Err(Error::Other(anyhow::anyhow!(
863 "Failed to extract public key: {}",
864 String::from_utf8_lossy(&output.stderr)
865 )));
866 }
867
868 Ok(())
869}
870
871fn dump_recent_logs(network: &TestNetwork) -> Result<()> {
872 const MAX_LOG_LINES: usize = 200;
873
874 let mut logs = network.read_logs()?;
875 let total = logs.len();
876 if total > MAX_LOG_LINES {
877 logs.drain(0..(total - MAX_LOG_LINES));
878 }
879
880 eprintln!(
881 "\n--- Network connectivity check failed; showing {} of {} log entries ---",
882 logs.len(),
883 total
884 );
885
886 for entry in logs {
887 let level = entry.level.as_deref().unwrap_or("INFO");
888 let ts_display = entry
889 .timestamp_raw
890 .clone()
891 .or_else(|| entry.timestamp.map(|ts| ts.to_rfc3339()));
892
893 if let Some(ts) = ts_display {
894 eprintln!("[{}] [{}] {}: {}", entry.peer_id, ts, level, entry.message);
895 } else {
896 eprintln!("[{}] {}: {}", entry.peer_id, level, entry.message);
897 }
898 }
899
900 eprintln!("--- End of network logs ---\n");
901
902 Ok(())
903}
904
905fn preserve_network_state(network: &TestNetwork) -> Result<PathBuf> {
906 Ok(network.run_root().to_path_buf())
907}