diff --git a/Cargo.lock b/Cargo.lock index fa8eb5c80..361a54f9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1764,6 +1764,7 @@ dependencies = [ "flatbuffers 25.9.23", "freenet-macros 0.1.0", "freenet-stdlib", + "freenet-test-network", "futures 0.3.31", "headers", "hickory-resolver", @@ -1912,6 +1913,26 @@ dependencies = [ "web-sys", ] +[[package]] +name = "freenet-test-network" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06be6aef3bb0433a963d0cc0c0f9b7d05e50b54fcb929e405fefab10d3b2db9" +dependencies = [ + "anyhow", + "chrono", + "freenet-stdlib", + "futures 0.3.31", + "serde", + "serde_json", + "sysinfo", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite 0.27.0", + "tracing", + "which", +] + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -2530,7 +2551,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -3257,6 +3278,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3369,12 +3399,31 @@ dependencies = [ "objc2-encode", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "objc2-encode" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.32.2" @@ -5181,6 +5230,20 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "sysinfo" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.61.3", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -5520,7 +5583,9 @@ checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", + "native-tls", "tokio", + "tokio-native-tls", "tungstenite 0.27.0", ] @@ -5872,6 +5937,7 @@ dependencies = [ "http 1.3.1", "httparse", "log", + "native-tls", "rand 0.9.2", "sha1", "thiserror 2.0.17", @@ -6530,16 +6596,38 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + [[package]] name = "windows" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", ] [[package]] @@ -6548,7 +6636,20 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-core", + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -6564,15 +6665,26 @@ dependencies = [ "windows-strings 0.5.1", ] +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + [[package]] name = "windows-future" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "windows-core", + "windows-core 0.62.2", "windows-link 0.2.1", - "windows-threading", + "windows-threading 0.2.1", ] [[package]] @@ -6609,13 +6721,23 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-numerics" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-core", + "windows-core 0.62.2", "windows-link 0.2.1", ] @@ -6759,6 +6881,15 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-threading" version = "0.2.1" @@ -6945,8 +7076,8 @@ dependencies = [ "log", "serde", "thiserror 2.0.17", - "windows", - "windows-core", + "windows 0.62.2", + "windows-core 0.62.2", ] [[package]] diff --git a/apps/freenet-ping/Cargo.lock b/apps/freenet-ping/Cargo.lock index bc38c19d2..32a378d2e 100644 --- a/apps/freenet-ping/Cargo.lock +++ b/apps/freenet-ping/Cargo.lock @@ -1309,6 +1309,7 @@ dependencies = [ "hickory-resolver", "itertools 0.14.0", "notify", + "once_cell", "opentelemetry", "parking_lot", "pav_regression", @@ -1398,9 +1399,9 @@ dependencies = [ [[package]] name = "freenet-stdlib" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c64fa03f4a083918c7e347be47122c223d8156f4c012a0fe8e89a643350f2d" +checksum = "f39e2953b4b0d82dd02458653b57166ba8c967c6b3fcec146102a27e05a7081a" dependencies = [ "arbitrary", "bincode", diff --git a/apps/freenet-ping/app/Cargo.toml b/apps/freenet-ping/app/Cargo.toml index ef83d63ae..a53670594 100644 --- a/apps/freenet-ping/app/Cargo.toml +++ b/apps/freenet-ping/app/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [features] testing = ["freenet-stdlib/testing", "freenet/testing"] +manual-tests = [] [dependencies] anyhow = "1.0" diff --git a/apps/freenet-ping/app/src/ping_client.rs b/apps/freenet-ping/app/src/ping_client.rs index 60e37f6ec..a8396a64f 100644 --- a/apps/freenet-ping/app/src/ping_client.rs +++ b/apps/freenet-ping/app/src/ping_client.rs @@ -47,7 +47,7 @@ pub async fn wait_for_put_response( expected_key: &ContractKey, ) -> Result> { loop { - let resp = timeout(Duration::from_secs(30), client.recv()).await; + let resp = timeout(Duration::from_secs(60), client.recv()).await; match resp { Ok(Ok(HostResponse::ContractResponse(ContractResponse::PutResponse { key }))) => { if &key == expected_key { @@ -91,7 +91,7 @@ pub async fn wait_for_get_response( expected_key: &ContractKey, ) -> Result> { loop { - let resp = timeout(Duration::from_secs(30), client.recv()).await; + let resp = timeout(Duration::from_secs(60), client.recv()).await; match resp { Ok(Ok(HostResponse::ContractResponse(ContractResponse::GetResponse { key, @@ -134,7 +134,7 @@ pub async fn wait_for_subscribe_response( expected_key: &ContractKey, ) -> Result<(), Box> { loop { - let resp = timeout(Duration::from_secs(30), client.recv()).await; + let resp = timeout(Duration::from_secs(60), client.recv()).await; match resp { Ok(Ok(HostResponse::ContractResponse(ContractResponse::SubscribeResponse { key, diff --git a/apps/freenet-ping/app/tests/README.md b/apps/freenet-ping/app/tests/README.md index 18e480be9..9965333fd 100644 --- a/apps/freenet-ping/app/tests/README.md +++ b/apps/freenet-ping/app/tests/README.md @@ -88,10 +88,10 @@ Run a specific blocked peers test variant: cargo test test_ping_blocked_peers_simple ``` -Run the large-scale partial connectivity network test: +Run the large-scale partial connectivity network test (requires the manual test feature because the scenario is still experimental): ```bash -cargo test -p freenet-ping-app --test run_app_partially_connected_network +cargo test -p freenet-ping-app --features manual-tests --test run_app test_ping_partially_connected_network ``` --- diff --git a/apps/freenet-ping/app/tests/common/mod.rs b/apps/freenet-ping/app/tests/common/mod.rs index 3a333a12a..a2206671c 100644 --- a/apps/freenet-ping/app/tests/common/mod.rs +++ b/apps/freenet-ping/app/tests/common/mod.rs @@ -208,6 +208,9 @@ pub(crate) enum PackageType { Delegate, } +const CONTRACT_EXTRA_FEATURES: [&str; 1] = ["contract"]; +const NO_EXTRA_FEATURES: [&str; 0] = []; + impl PackageType { pub fn feature(&self) -> &'static str { match self { @@ -215,6 +218,13 @@ impl PackageType { PackageType::Delegate => "freenet-main-delegate", } } + + pub fn extra_features(&self) -> &'static [&'static str] { + match self { + PackageType::Contract => &CONTRACT_EXTRA_FEATURES, + PackageType::Delegate => &NO_EXTRA_FEATURES, + } + } } impl std::fmt::Display for PackageType { @@ -250,9 +260,10 @@ fn compile_options(cli_config: &BuildToolConfig) -> impl Iterator .iter() .flat_map(|s| { s.split(',') - .filter(|p| *p != cli_config.package_type.feature()) + .filter(|p| *p != cli_config.package_type.feature() && *p != "contract") }) - .chain([cli_config.package_type.feature()]); + .chain([cli_config.package_type.feature()]) + .chain(cli_config.package_type.extra_features().iter().copied()); let features = [ "--features".to_string(), feature_list.collect::>().join(","), @@ -262,7 +273,33 @@ fn compile_options(cli_config: &BuildToolConfig) -> impl Iterator .chain(release.iter().map(|s| s.to_string())) } // TODO: refactor so we share the implementation with fdev (need to extract to ) +fn ensure_target_dir_env() { + if std::env::var(TARGET_DIR_VAR).is_err() { + let workspace_dir = std::env::var("CARGO_WORKSPACE_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| find_workspace_root()); + let target_dir = workspace_dir.join("target"); + std::env::set_var(TARGET_DIR_VAR, &target_dir); + } +} + +fn find_workspace_root() -> PathBuf { + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + manifest_dir + .ancestors() + .find(|dir| { + let cargo_toml = dir.join("Cargo.toml"); + cargo_toml.exists() + && std::fs::read_to_string(&cargo_toml) + .map(|contents| contents.contains("[workspace]")) + .unwrap_or(false) + }) + .expect("Could not determine workspace root from manifest directory") + .to_path_buf() +} + fn compile_contract(contract_path: &PathBuf) -> anyhow::Result> { + ensure_target_dir_env(); println!("module path: {contract_path:?}"); let target = std::env::var(TARGET_DIR_VAR) .map_err(|_| anyhow::anyhow!("CARGO_TARGET_DIR should be set"))?; diff --git a/apps/freenet-ping/app/tests/run_app.rs b/apps/freenet-ping/app/tests/run_app.rs index b1cd6480d..a6a7c2009 100644 --- a/apps/freenet-ping/app/tests/run_app.rs +++ b/apps/freenet-ping/app/tests/run_app.rs @@ -1519,8 +1519,8 @@ async fn test_ping_application_loop() -> TestResult { Ok(()) } +#[cfg(feature = "manual-tests")] #[tokio::test(flavor = "multi_thread")] -#[ignore = "Test has never worked - gateway nodes fail on startup with channel closed errors"] async fn test_ping_partially_connected_network() -> TestResult { /* * This test verifies how subscription propagation works in a partially connected network. @@ -1750,15 +1750,18 @@ async fn test_ping_partially_connected_network() -> TestResult { i, NUM_GATEWAYS, num_connections); } - // Load the ping contract + // Load the ping contract. Compile once to determine the code hash, then again with proper options. let path_to_code = PathBuf::from(PACKAGE_DIR).join(PATH_TO_CONTRACT); tracing::info!(path=%path_to_code.display(), "loading contract code"); - let code = std::fs::read(path_to_code) - .ok() - .ok_or_else(|| anyhow!("Failed to read contract code"))?; - let code_hash = CodeHash::from_code(&code); - - // Create ping contract options + let temp_options = PingContractOptions { + frequency: Duration::from_secs(3), + ttl: Duration::from_secs(60), + tag: APP_TAG.to_string(), + code_key: String::new(), + }; + let temp_params = Parameters::from(serde_json::to_vec(&temp_options).unwrap()); + let temp_container = common::load_contract(&path_to_code, temp_params)?; + let code_hash = CodeHash::from_code(temp_container.data()); let ping_options = PingContractOptions { frequency: Duration::from_secs(3), ttl: Duration::from_secs(60), @@ -1767,7 +1770,7 @@ async fn test_ping_partially_connected_network() -> TestResult { }; let params = Parameters::from(serde_json::to_vec(&ping_options).unwrap()); - let container = ContractContainer::try_from((code, ¶ms))?; + let container = common::load_contract(&path_to_code, params)?; let contract_key = container.key(); // Choose a node to publish the contract diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index eb535d22d..1a5d7f6aa 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -90,6 +90,7 @@ arbitrary = { features = ["derive"], version = "1" } chrono = { features = ["arbitrary"], workspace = true } freenet-stdlib = { features = ["net", "testing"], workspace = true } freenet-macros = { path = "../freenet-macros" } +freenet-test-network = "0.1.1" httptest = "0.16" statrs = "0.18" tempfile = "3" diff --git a/crates/core/tests/diagnose_connectivity.rs b/crates/core/tests/diagnose_connectivity.rs new file mode 100644 index 000000000..e6096c33f --- /dev/null +++ b/crates/core/tests/diagnose_connectivity.rs @@ -0,0 +1,49 @@ +//! Diagnostic test to understand connectivity failures + +use freenet_test_network::{BuildProfile, FreenetBinary, TestNetwork}; +use std::time::Duration; + +#[tokio::test] +async fn diagnose_connectivity_failure() { + // Build network with more relaxed settings + let result = TestNetwork::builder() + .gateways(1) + .peers(2) + .binary(FreenetBinary::CurrentCrate(BuildProfile::Debug)) + .require_connectivity(0.5) // Lower threshold - just need 50% + .connectivity_timeout(Duration::from_secs(60)) // Longer timeout + .preserve_temp_dirs_on_failure(true) + .build() + .await; + + match result { + Ok(network) => { + println!("\n✓ Network started successfully!"); + + // Print network info + println!("\nNetwork topology:"); + println!(" Gateway: {}", network.gateway(0).ws_url()); + for i in 0..2 { + println!(" Peer {}: {}", i, network.peer(i).ws_url()); + } + + // Read and print logs + println!("\n=== Network Logs ==="); + if let Ok(logs) = network.read_logs() { + for entry in logs.iter().take(200) { + println!( + "[{}] {}: {}", + entry.peer_id, + entry.level.as_deref().unwrap_or("INFO"), + entry.message + ); + } + println!("\n(Showing first 200 log lines, total: {})", logs.len()); + } + } + Err(e) => { + eprintln!("\n✗ Network failed to start: {:?}", e); + panic!("Network startup failed - see logs above"); + } + } +} diff --git a/crates/core/tests/manual_network_test.rs b/crates/core/tests/manual_network_test.rs new file mode 100644 index 000000000..f00de51da --- /dev/null +++ b/crates/core/tests/manual_network_test.rs @@ -0,0 +1,47 @@ +//! Manual test to inspect network logs + +use freenet_test_network::{BuildProfile, FreenetBinary, TestNetwork}; +use std::time::Duration; + +#[tokio::test] +#[ignore] // Run manually with: cargo test manual_network_test -- --ignored --nocapture +async fn manual_network_test() { + let network = TestNetwork::builder() + .gateways(1) + .peers(1) + .binary(FreenetBinary::CurrentCrate(BuildProfile::Debug)) + .require_connectivity(0.5) + .connectivity_timeout(Duration::from_secs(10)) // Short timeout so we can inspect quickly + .preserve_temp_dirs_on_failure(true) + .build() + .await; + + match network { + Ok(ref net) => { + println!("\n=== Network Started ==="); + println!("Gateway: {}", net.gateway(0).ws_url()); + println!("Peer: {}", net.peer(0).ws_url()); + + // Print all logs + if let Ok(logs) = net.read_logs() { + println!("\n=== Logs ==="); + for entry in logs { + println!( + "[{}] {}: {}", + entry.peer_id, + entry.level.as_deref().unwrap_or("INFO"), + entry.message + ); + } + } + + // Keep network alive for inspection + println!("\nNetwork is running. Press Ctrl+C to exit."); + tokio::time::sleep(Duration::from_secs(300)).await; + } + Err(e) => { + eprintln!("\n✗ Network failed: {:?}", e); + // Try to read logs anyway if temp dirs still exist + } + } +} diff --git a/crates/core/tests/test_network_integration.rs b/crates/core/tests/test_network_integration.rs new file mode 100644 index 000000000..f433ec932 --- /dev/null +++ b/crates/core/tests/test_network_integration.rs @@ -0,0 +1,66 @@ +//! Integration test demonstrating freenet-test-network usage +//! +//! This shows how much simpler tests become with the test-network crate + +use freenet_stdlib::client_api::WebApi; +use freenet_test_network::TestNetwork; +use testresult::TestResult; +use tokio_tungstenite::connect_async; + +// Helper to get or create network +async fn get_network() -> &'static TestNetwork { + use tokio::sync::OnceCell; + static NETWORK: OnceCell = OnceCell::const_new(); + + NETWORK + .get_or_init(|| async { + TestNetwork::builder() + .gateways(1) + .peers(2) + .binary(freenet_test_network::FreenetBinary::CurrentCrate( + freenet_test_network::BuildProfile::Debug, + )) + .build() + .await + .expect("Failed to start test network") + }) + .await +} + +#[tokio::test] +async fn test_network_connectivity() -> TestResult { + let network = get_network().await; + + // Just verify we can connect to all peers + let gw_url = format!("{}?encodingProtocol=native", network.gateway(0).ws_url()); + let (stream, _) = connect_async(&gw_url).await?; + let _gw_client = WebApi::start(stream); + + let peer_url = format!("{}?encodingProtocol=native", network.peer(0).ws_url()); + let (stream, _) = connect_async(&peer_url).await?; + let _peer_client = WebApi::start(stream); + + println!("✓ Successfully connected to gateway and peer"); + Ok(()) +} + +#[tokio::test] +async fn test_multiple_connections() -> TestResult { + let network = get_network().await; + + // Each test gets its own connections - no conflicts + let url1 = format!("{}?encodingProtocol=native", network.gateway(0).ws_url()); + let (stream1, _) = connect_async(&url1).await?; + let _client1 = WebApi::start(stream1); + + let url2 = format!("{}?encodingProtocol=native", network.peer(0).ws_url()); + let (stream2, _) = connect_async(&url2).await?; + let _client2 = WebApi::start(stream2); + + let url3 = format!("{}?encodingProtocol=native", network.peer(1).ws_url()); + let (stream3, _) = connect_async(&url3).await?; + let _client3 = WebApi::start(stream3); + + println!("✓ Multiple WebSocket connections work"); + Ok(()) +}