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 "--telemetry-enabled".to_string(),
432 "false".to_string(),
433 ];
434
435 if is_gateway {
436 args.push("--is-gateway".to_string());
437 }
438
439 args.push("--transport-keypair".to_string());
440 let keypair_arg = match &location {
441 PeerLocation::Local => data_dir.join("keypair.pem").to_string_lossy().to_string(),
442 PeerLocation::Remote(remote) => remote
443 .remote_work_dir()
444 .join(&id)
445 .join("keypair.pem")
446 .to_string_lossy()
447 .to_string(),
448 };
449 args.push(keypair_arg);
450
451 if !is_gateway && !gateway_info.is_empty() {
453 let gateways_toml = data_dir.join("gateways.toml");
454 let mut content = String::new();
455 for gw in gateway_info {
456 let gw_pubkey_path = match &location {
457 PeerLocation::Local => gw.public_key_path.clone(),
458 PeerLocation::Remote(remote) => {
459 let gw_pubkey_name = gw.public_key_path.file_name().ok_or_else(|| {
460 Error::PeerStartupFailed("Invalid gateway pubkey path".to_string())
461 })?;
462 remote.remote_work_dir().join(&id).join(gw_pubkey_name)
463 }
464 };
465 content.push_str(&format!(
466 "[[gateways]]\n\
467 address = {{ hostname = \"{}\" }}\n\
468 public_key = \"{}\"\n\n",
469 gw.address,
470 gw_pubkey_path.display()
471 ));
472 }
473 std::fs::write(&gateways_toml, content)?;
474
475 if let PeerLocation::Remote(remote) = &location {
477 let remote_gateways_toml = remote.remote_work_dir().join(&id).join("gateways.toml");
478 remote.scp_upload(&gateways_toml, remote_gateways_toml.to_str().unwrap())?;
479 }
480 }
481
482 let env_vars = vec![
484 ("NETWORK_ADDRESS".to_string(), network_address.clone()),
485 (
486 "PUBLIC_NETWORK_ADDRESS".to_string(),
487 network_address.clone(),
488 ),
489 ("PUBLIC_NETWORK_PORT".to_string(), network_port.to_string()),
490 ];
491
492 if let Some(min_conn) = self.min_connections {
493 args.push("--min-number-of-connections".to_string());
494 args.push(min_conn.to_string());
495 }
496 if let Some(max_conn) = self.max_connections {
497 args.push("--max-number-of-connections".to_string());
498 args.push(max_conn.to_string());
499 }
500
501 let process: Box<dyn PeerProcess + Send> = match &location {
503 PeerLocation::Local => Box::new(process::spawn_local_peer(
504 binary_path,
505 &args,
506 &data_dir,
507 &env_vars,
508 )?),
509 PeerLocation::Remote(remote) => {
510 let remote_data_dir = remote.remote_work_dir().join(&id);
511 let local_cache_dir = run_root.join(format!("{}-cache", id));
512 std::fs::create_dir_all(&local_cache_dir)?;
513
514 Box::new(
515 process::spawn_remote_peer(
516 binary_path,
517 &args,
518 remote,
519 &remote_data_dir,
520 &local_cache_dir,
521 &env_vars,
522 )
523 .await?,
524 )
525 }
526 };
527
528 tokio::time::sleep(Duration::from_millis(100)).await;
530
531 Ok(TestPeer {
532 id,
533 is_gateway,
534 ws_port,
535 network_port,
536 network_address,
537 data_dir,
538 process,
539 public_key_path: Some(public_key_path),
540 location,
541 })
542 }
543
544 async fn build_docker_nat(self, config: DockerNatConfig) -> Result<TestNetwork> {
546 let binary_path = self.binary.resolve()?;
547
548 tracing::info!(
549 "Starting Docker NAT test network: {} gateways, {} peers",
550 self.gateways,
551 self.peers
552 );
553
554 let base_dir = resolve_base_dir();
555 fs::create_dir_all(&base_dir)?;
556 cleanup_old_runs(&base_dir, 5)?;
557 let run_root = create_run_directory(&base_dir)?;
558
559 let mut run_status = RunStatusGuard::new(&run_root);
560
561 let mut docker_backend = DockerNatBackend::new(config).await.map_err(|e| {
563 run_status.mark("failure", Some(&format!("Docker init failed: {}", e)));
564 e
565 })?;
566
567 docker_backend.create_public_network().await.map_err(|e| {
569 run_status.mark(
570 "failure",
571 Some(&format!("Failed to create public network: {}", e)),
572 );
573 e
574 })?;
575
576 let ws_port: u16 = 9000;
578 let network_port: u16 = 31337;
579
580 let mut gateways = Vec::new();
582 for i in 0..self.gateways {
583 let data_dir = create_peer_dir(&run_root, &format!("gw{}", i))?;
584
585 let keypair_path = data_dir.join("keypair.pem");
587 let public_key_path = data_dir.join("public_key.pem");
588 generate_keypair(&keypair_path, &public_key_path)?;
589
590 let (info, process) = docker_backend
591 .create_gateway(
592 i,
593 &binary_path,
594 &keypair_path,
595 &public_key_path,
596 ws_port,
597 network_port,
598 &run_root,
599 )
600 .await
601 .map_err(|e| {
602 let detail = format!("failed to start gateway {}: {}", i, e);
603 run_status.mark("failure", Some(&detail));
604 e
605 })?;
606
607 let peer = TestPeer {
608 id: format!("gw{}", i),
609 is_gateway: true,
610 ws_port: info.host_ws_port,
611 network_port: info.network_port,
612 network_address: info.public_ip.to_string(),
613 data_dir,
614 process: Box::new(process),
615 public_key_path: Some(public_key_path),
616 location: PeerLocation::Local, };
618 gateways.push(peer);
619 }
620
621 let gateway_info: Vec<_> = gateways
623 .iter()
624 .map(|gw| GatewayInfo {
625 address: format!("{}:{}", gw.network_address, network_port),
626 public_key_path: gw
627 .public_key_path
628 .clone()
629 .expect("Gateway must have public key"),
630 })
631 .collect();
632
633 let mut peers = Vec::new();
635 for i in 0..self.peers {
636 let peer_index = i + self.gateways;
637 let data_dir = create_peer_dir(&run_root, &format!("peer{}", peer_index))?;
638
639 let keypair_path = data_dir.join("keypair.pem");
641 let public_key_path = data_dir.join("public_key.pem");
642 generate_keypair(&keypair_path, &public_key_path)?;
643
644 let gateways_toml_path = data_dir.join("gateways.toml");
646 let mut gateways_content = String::new();
647 for gw in &gateway_info {
648 gateways_content.push_str(&format!(
649 "[[gateways]]\n\
650 address = {{ hostname = \"{}\" }}\n\
651 public_key = \"/config/gw_public_key.pem\"\n\n",
652 gw.address,
653 ));
654 }
655 std::fs::write(&gateways_toml_path, &gateways_content)?;
656
657 let gateway_public_key_path = gateway_info.first().map(|gw| gw.public_key_path.clone());
659
660 let (info, process) = docker_backend
661 .create_peer(
662 peer_index,
663 &binary_path,
664 &keypair_path,
665 &public_key_path,
666 &gateways_toml_path,
667 gateway_public_key_path.as_deref(),
668 ws_port,
669 network_port,
670 &run_root,
671 )
672 .await
673 .map_err(|e| {
674 let detail = format!("failed to start peer {}: {}", peer_index, e);
675 run_status.mark("failure", Some(&detail));
676 e
677 })?;
678
679 let peer = TestPeer {
680 id: format!("peer{}", peer_index),
681 is_gateway: false,
682 ws_port: info.host_ws_port,
683 network_port: info.network_port,
684 network_address: info.private_ip.to_string(),
685 data_dir,
686 process: Box::new(process),
687 public_key_path: Some(public_key_path),
688 location: PeerLocation::Local,
689 };
690 peers.push(peer);
691
692 if i + 1 < self.peers && !self.start_stagger.is_zero() {
693 tokio::time::sleep(self.start_stagger).await;
694 }
695 }
696
697 let network = TestNetwork::new_with_docker(
699 gateways,
700 peers,
701 self.min_connectivity,
702 run_root.clone(),
703 Some(docker_backend),
704 );
705
706 match network
708 .wait_until_ready_with_timeout(self.connectivity_timeout)
709 .await
710 {
711 Ok(()) => {
712 if self.preserve_data_on_success {
713 println!(
714 "Network data directories preserved at {}",
715 run_root.display()
716 );
717 }
718 let detail = format!(
719 "success: gateways={}, peers={} (Docker NAT)",
720 self.gateways, self.peers
721 );
722 run_status.mark("success", Some(&detail));
723 Ok(network)
724 }
725 Err(err) => {
726 if let Err(log_err) = dump_recent_logs(&network) {
727 eprintln!("Failed to dump logs after connectivity error: {}", log_err);
728 }
729 if self.preserve_data_on_failure {
730 eprintln!(
731 "Network data directories preserved at {}",
732 run_root.display()
733 );
734 }
735 let detail = err.to_string();
736 run_status.mark("failure", Some(&detail));
737 Err(err)
738 }
739 }
740 }
741}
742
743fn resolve_base_dir() -> PathBuf {
744 if let Some(path) = std::env::var_os("FREENET_TEST_NETWORK_BASE_DIR") {
745 PathBuf::from(path)
746 } else if let Ok(home) = std::env::var("HOME") {
747 PathBuf::from(home).join("code/tmp/freenet-test-networks")
748 } else {
749 std::env::temp_dir().join("freenet-test-networks")
750 }
751}
752
753fn cleanup_old_runs(base_dir: &Path, max_runs: usize) -> Result<()> {
754 let mut runs: Vec<(PathBuf, SystemTime)> = fs::read_dir(base_dir)?
755 .filter_map(|entry| {
756 let entry = entry.ok()?;
757 let file_type = entry.file_type().ok()?;
758 if !file_type.is_dir() {
759 return None;
760 }
761 let metadata = entry.metadata().ok()?;
762 let modified = metadata.modified().unwrap_or(SystemTime::UNIX_EPOCH);
763 Some((entry.path(), modified))
764 })
765 .collect();
766
767 if runs.len() <= max_runs {
768 return Ok(());
769 }
770
771 runs.sort_by_key(|(_, modified)| *modified);
772 let remove_count = runs.len() - max_runs;
773 for (path, _) in runs.into_iter().take(remove_count) {
774 if let Err(err) = fs::remove_dir_all(&path) {
775 tracing::warn!(
776 ?err,
777 path = %path.display(),
778 "Failed to remove old freenet test network run directory"
779 );
780 }
781 }
782
783 Ok(())
784}
785
786fn create_run_directory(base_dir: &Path) -> Result<PathBuf> {
787 let timestamp = Utc::now().format("%Y%m%d-%H%M%S").to_string();
788 for attempt in 0..100 {
789 let candidate = if attempt == 0 {
790 base_dir.join(×tamp)
791 } else {
792 base_dir.join(format!("{}-{}", ×tamp, attempt))
793 };
794 if !candidate.exists() {
795 fs::create_dir_all(&candidate)?;
796 return Ok(candidate);
797 }
798 }
799
800 Err(Error::Other(anyhow::anyhow!(
801 "Unable to allocate run directory after repeated attempts"
802 )))
803}
804
805fn create_peer_dir(run_root: &Path, id: &str) -> Result<PathBuf> {
806 let dir = run_root.join(id);
807 fs::create_dir_all(&dir)?;
808 Ok(dir)
809}
810
811struct RunStatusGuard {
812 status_path: PathBuf,
813}
814
815impl RunStatusGuard {
816 fn new(run_root: &Path) -> Self {
817 let status_path = run_root.join("run_status.txt");
818 let _ = fs::write(&status_path, b"status=initializing\n");
819 Self { status_path }
820 }
821
822 fn mark(&mut self, status: &str, detail: Option<&str>) {
823 let mut content = format!("status={}", status);
824 if let Some(detail) = detail {
825 content.push('\n');
826 content.push_str("detail=");
827 content.push_str(detail);
828 }
829 content.push('\n');
830 if let Err(err) = fs::write(&self.status_path, content) {
831 tracing::warn!(
832 ?err,
833 path = %self.status_path.display(),
834 "Failed to write run status"
835 );
836 }
837 }
838}
839
840fn generate_keypair(
841 private_key_path: &std::path::Path,
842 public_key_path: &std::path::Path,
843) -> Result<()> {
844 use rand::RngCore;
845 use x25519_dalek::{PublicKey, StaticSecret};
846
847 let mut secret_bytes = [0u8; 32];
849 rand::thread_rng().fill_bytes(&mut secret_bytes);
850
851 let secret = StaticSecret::from(secret_bytes);
853 let public = PublicKey::from(&secret);
854 drop(secret); std::fs::write(private_key_path, hex::encode(secret_bytes))
858 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to write private key: {}", e)))?;
859
860 std::fs::write(public_key_path, hex::encode(public.as_bytes()))
862 .map_err(|e| Error::Other(anyhow::anyhow!("Failed to write public key: {}", e)))?;
863
864 Ok(())
865}
866
867fn dump_recent_logs(network: &TestNetwork) -> Result<()> {
868 const MAX_LOG_LINES: usize = 200;
869
870 let mut logs = network.read_logs()?;
871 let total = logs.len();
872 if total > MAX_LOG_LINES {
873 logs.drain(0..(total - MAX_LOG_LINES));
874 }
875
876 eprintln!(
877 "\n--- Network connectivity check failed; showing {} of {} log entries ---",
878 logs.len(),
879 total
880 );
881
882 for entry in logs {
883 let level = entry.level.as_deref().unwrap_or("INFO");
884 let ts_display = entry
885 .timestamp_raw
886 .clone()
887 .or_else(|| entry.timestamp.map(|ts| ts.to_rfc3339()));
888
889 if let Some(ts) = ts_display {
890 eprintln!("[{}] [{}] {}: {}", entry.peer_id, ts, level, entry.message);
891 } else {
892 eprintln!("[{}] {}: {}", entry.peer_id, level, entry.message);
893 }
894 }
895
896 eprintln!("--- End of network logs ---\n");
897
898 Ok(())
899}
900
901fn preserve_network_state(network: &TestNetwork) -> Result<PathBuf> {
902 Ok(network.run_root().to_path_buf())
903}