Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,45 @@ jobs:
- name: Test
run: cargo test --workspace --no-default-features --features trace,websocket,redb

ubertest:
name: Ubertest
needs: test_all

runs-on: freenet-default-runner

env:
FREENET_LOG: error
CARGO_TARGET_DIR: ${{ github.workspace }}/target
UBERTEST_PEER_COUNT: 6 # Fewer peers for faster CI

steps:
- name: Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: ${{ github.token }}

- uses: actions/checkout@v5

- uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: wasm32-unknown-unknown

- uses: Swatinem/rust-cache@v2
if: success() || steps.test.conclusion == 'failure'
with:
save-if: false

- name: Install riverctl
run: cargo install riverctl

- name: Build
run: cargo build --locked

- name: Run Ubertest
run: cargo test --test ubertest --no-default-features --features trace,websocket,redb
working-directory: crates/core

clippy_check:
name: Clippy

Expand Down
161 changes: 143 additions & 18 deletions crates/core/tests/ubertest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ struct PeerInfo {
ws_port: u16,
temp_dir: tempfile::TempDir,
_is_gateway: bool,
location: f64,
}

/// Check that riverctl is installed and is the latest version
Expand Down Expand Up @@ -141,16 +142,11 @@ async fn create_peer_config(
key.save(&transport_keypair)?;
key.public().save(temp_dir.path().join("public.pem"))?;

// Use different loopback IPs to ensure unique ring locations for P2P network
// 127.0.0.1 is gateway, 127.1-255.x.1 for peers
let peer_ip = if is_gateway {
Ipv4Addr::new(127, 0, 0, 1)
} else {
// Randomize 2nd and 3rd bytes to minimize location collisions
// Bind all peers to localhost; macOS only routes 127.0.0.1 by default.
let peer_ip = Ipv4Addr::LOCALHOST;
let location = {
let mut rng = RNG.lock().unwrap();
let byte2 = rng.random_range(1..=255);
let byte3 = rng.random_range(0..=255);
Ipv4Addr::new(127, byte2, byte3, 1)
rng.random()
};

// Bind network socket to peer-specific IP for P2P communication
Expand Down Expand Up @@ -179,12 +175,12 @@ async fn create_peer_config(
ws_api_port: Some(ws_port),
},
network_api: NetworkArgs {
public_address: Some(peer_ip.into()), // Use randomized IP for P2P network
public_address: Some(peer_ip.into()), // Share localhost IP for P2P network
public_port: Some(network_port), // Always set for localhost (required for local networks)
is_gateway,
skip_load_from_network: true,
gateways: Some(gateways),
location: None, // Let location be derived from IP address
location: Some(location), // Ensure unique ring location even with shared IP
ignore_protocol_checking: true,
address: Some(peer_ip.into()),
network_port: Some(network_port),
Expand All @@ -208,6 +204,7 @@ async fn create_peer_config(
ws_port,
temp_dir,
_is_gateway: is_gateway,
location,
};

Ok((config, peer_info))
Expand Down Expand Up @@ -255,8 +252,135 @@ async fn verify_network_topology(
Ok(true)
}

/// Simplified test with just gateway + 1 peer to verify basic PUT operations work
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_basic_room_creation() -> anyhow::Result<()> {
freenet::config::set_logger(Some(tracing::level_filters::LevelFilter::DEBUG), None);

info!("=== Basic Room Creation Test ===");
info!("Testing minimal setup: 1 gateway + 1 peer");

// Find riverctl without version check
let riverctl_path = which::which("riverctl").context("riverctl not found in PATH")?;
info!("Using riverctl at: {}", riverctl_path.display());

// Create gateway
let (gw_config, gw_info) = create_peer_config("gateway".to_string(), true, None, 0).await?;
let gateway_inline_config = InlineGwConfig {
address: (Ipv4Addr::LOCALHOST, gw_info.network_port).into(),
location: Some(gw_info.location),
public_key_path: gw_info.temp_dir.path().join("public.pem"),
};

info!(
"Gateway - network: {}, ws: {}",
gw_info.network_port, gw_info.ws_port
);

// Create peer
let (peer_config, peer_info) = create_peer_config(
"peer0".to_string(),
false,
Some(gateway_inline_config.clone()),
1,
)
.await?;

info!(
"Peer - network: {}, ws: {}",
peer_info.network_port, peer_info.ws_port
);

// Start gateway
let gw_node = async {
let config = gw_config.build().await?;
let node = NodeConfig::new(config.clone())
.await?
.build(serve_gateway(config.ws_api).await)
.await?;
node.run().await
}
.boxed_local();

// Start peer (with delay for gateway to be ready)
let peer_node = async {
sleep(Duration::from_secs(5)).await;
let config = peer_config.build().await?;
let node = NodeConfig::new(config.clone())
.await?
.build(serve_gateway(config.ws_api).await)
.await?;
node.run().await
}
.boxed_local();

let peer_ws_port = peer_info.ws_port;
let peer_temp_dir = peer_info.temp_dir.path().to_path_buf();

// Test logic
let test_logic = timeout(Duration::from_secs(120), async move {
info!("Waiting for nodes to bootstrap...");
sleep(Duration::from_secs(25)).await;

let peer_ws = format!(
"ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native",
peer_ws_port
);
let river_config_dir = peer_temp_dir.join("river-user0");
std::fs::create_dir_all(&river_config_dir)?;

info!("Creating room via riverctl...");
let output = Command::new(&riverctl_path)
.env("RIVER_CONFIG_DIR", &river_config_dir)
.args([
"--node-url",
&peer_ws,
"--format",
"json",
"room",
"create",
"--name",
"test-room",
"--nickname",
"Alice",
])
.output()
.context("Failed to execute riverctl room create")?;

if !output.status.success() {
bail!(
"Room creation failed: {}\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stderr),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}

info!("✓ Room created successfully");
info!("Output: {}", String::from_utf8_lossy(&output.stdout));
Ok::<(), anyhow::Error>(())
});

// Run everything
select! {
result = test_logic => {
result??;
info!("Test completed successfully");
}
result = gw_node => {
result?;
bail!("Gateway node exited unexpectedly");
}
result = peer_node => {
result?;
bail!("Peer node exited unexpectedly");
}
}

Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 8)]
#[ignore = "Requires riverctl to be installed - run manually with: cargo test --test ubertest -- --ignored"]
async fn test_app_ubertest() -> anyhow::Result<()> {
freenet::config::set_logger(Some(tracing::level_filters::LevelFilter::DEBUG), None);

Expand All @@ -276,7 +400,7 @@ async fn test_app_ubertest() -> anyhow::Result<()> {

let gateway_inline_config = InlineGwConfig {
address: (Ipv4Addr::LOCALHOST, gw_info.network_port).into(),
location: Some(RNG.lock().unwrap().random()),
location: Some(gw_info.location),
public_key_path: gw_info.temp_dir.path().join("public.pem"),
};

Expand All @@ -294,6 +418,10 @@ async fn test_app_ubertest() -> anyhow::Result<()> {
}
.boxed_local();

// Wait for gateway startup
sleep(Duration::from_secs(20)).await;
info!("Gateway started, peers starting with 20s delays...");

// Step 3: Create and start peers with staggered startup
info!(
"\n--- Step 3: Creating {} Peers (staggered startup) ---",
Expand Down Expand Up @@ -343,13 +471,10 @@ async fn test_app_ubertest() -> anyhow::Result<()> {
// The actual test logic
let test_logic = timeout(Duration::from_secs(600), async {
info!("\n--- Step 4: Waiting for Network Formation ---");
// Wait for gateway startup
sleep(Duration::from_secs(20)).await;
info!("Gateway started, peers starting with 10s delays...");

// Wait for all peers to start (last peer starts after peer_count * 10 seconds)
let peer_startup_time = config.peer_count * 10 + 30; // Extra 30s buffer
sleep(Duration::from_secs(peer_startup_time as u64)).await;
//let peer_startup_time = config.peer_count * 10 + 30; // Extra 30s buffer
sleep(Duration::from_secs(30)).await;
info!("All peers should be started now");

// Wait additional time for mesh formation (connection maintenance cycles)
Expand Down
Loading