Skip to content

Commit 9d1683f

Browse files
committed
fix: make three-hop put test deterministic
1 parent d3ce033 commit 9d1683f

File tree

4 files changed

+357
-10
lines changed

4 files changed

+357
-10
lines changed

crates/core/tests/operations.rs

Lines changed: 69 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1909,9 +1909,49 @@ async fn test_delegate_request(ctx: &mut TestContext) -> TestResult {
19091909
/// Expected flow:
19101910
/// 1. peer-a sends PUT → routes through gateway → stored on peer-c
19111911
/// 2. peer-c sends PUT response → routes back through gateway → received by peer-a
1912+
const THREE_HOP_TEST_CONTRACT: &str = "test-contract-integration";
1913+
1914+
static THREE_HOP_CONTRACT: LazyLock<(ContractContainer, freenet::dev_tool::Location)> =
1915+
LazyLock::new(|| {
1916+
let contract =
1917+
test_utils::load_contract(THREE_HOP_TEST_CONTRACT, vec![].into()).expect("contract");
1918+
let location = freenet::dev_tool::Location::from(&contract.key());
1919+
(contract, location)
1920+
});
1921+
1922+
fn three_hop_contract_location() -> freenet::dev_tool::Location {
1923+
let (_, location) = &*THREE_HOP_CONTRACT;
1924+
*location
1925+
}
1926+
1927+
fn three_hop_gateway_location() -> f64 {
1928+
freenet::dev_tool::Location::new_rounded(three_hop_contract_location().as_f64() + 0.2).as_f64()
1929+
}
1930+
1931+
fn three_hop_peer_a_location() -> f64 {
1932+
freenet::dev_tool::Location::new_rounded(three_hop_contract_location().as_f64() + 0.5).as_f64()
1933+
}
1934+
1935+
fn three_hop_peer_c_location() -> f64 {
1936+
three_hop_contract_location().as_f64()
1937+
}
1938+
1939+
fn expected_three_hop_locations() -> [f64; 3] {
1940+
[
1941+
three_hop_gateway_location(),
1942+
three_hop_peer_a_location(),
1943+
three_hop_peer_c_location(),
1944+
]
1945+
}
1946+
19121947
#[freenet_test(
19131948
nodes = ["gateway", "peer-a", "peer-c"],
19141949
gateways = ["gateway"],
1950+
node_configs = {
1951+
"gateway": { location: three_hop_gateway_location() },
1952+
"peer-a": { location: three_hop_peer_a_location() },
1953+
"peer-c": { location: three_hop_peer_c_location() },
1954+
},
19151955
auto_connect_peers = true,
19161956
timeout_secs = 240,
19171957
startup_wait_secs = 15,
@@ -1922,10 +1962,12 @@ async fn test_delegate_request(ctx: &mut TestContext) -> TestResult {
19221962
async fn test_put_contract_three_hop_returns_response(ctx: &mut TestContext) -> TestResult {
19231963
use freenet::dev_tool::Location;
19241964

1925-
const TEST_CONTRACT: &str = "test-contract-integration";
1926-
let contract = test_utils::load_contract(TEST_CONTRACT, vec![].into())?;
1965+
let (contract, contract_location) = {
1966+
let (contract, location) = &*THREE_HOP_CONTRACT;
1967+
(contract.clone(), *location)
1968+
};
19271969
let contract_key = contract.key();
1928-
let contract_location = Location::from(&contract_key);
1970+
let node_locations = expected_three_hop_locations();
19291971

19301972
let initial_state = test_utils::create_empty_todo_list();
19311973
let wrapped_state = WrappedState::from(initial_state);
@@ -1935,16 +1977,36 @@ async fn test_put_contract_three_hop_returns_response(ctx: &mut TestContext) ->
19351977
let peer_a = ctx.node("peer-a")?;
19361978
let peer_c = ctx.node("peer-c")?;
19371979

1938-
// Note: We cannot modify node locations after they're created with the macro,
1939-
// so this test will use random locations. The original test had specific location
1940-
// requirements to ensure proper three-hop routing. For now, we'll proceed with
1941-
// the test and it should still validate PUT response routing.
1980+
assert_eq!(gateway.location, node_locations[0]);
1981+
assert_eq!(peer_a.location, node_locations[1]);
1982+
assert_eq!(peer_c.location, node_locations[2]);
19421983

19431984
tracing::info!("Node A data dir: {:?}", peer_a.temp_dir_path);
19441985
tracing::info!("Gateway node data dir: {:?}", gateway.temp_dir_path);
19451986
tracing::info!("Node C data dir: {:?}", peer_c.temp_dir_path);
19461987
tracing::info!("Contract location: {}", contract_location.as_f64());
19471988

1989+
let gateway_distance = Location::new(gateway.location).distance(contract_location);
1990+
let peer_a_distance = Location::new(peer_a.location).distance(contract_location);
1991+
let peer_c_distance = Location::new(peer_c.location).distance(contract_location);
1992+
1993+
// Ensure the contract should naturally route to peer-c to create the 3-hop path:
1994+
// peer-a (client) -> gateway -> peer-c (closest to contract).
1995+
assert!(
1996+
peer_c_distance.as_f64() < gateway_distance.as_f64(),
1997+
"peer-c must be closer to contract than the gateway for three-hop routing"
1998+
);
1999+
assert!(
2000+
peer_c_distance.as_f64() < peer_a_distance.as_f64(),
2001+
"peer-c must be closest node to the contract location"
2002+
);
2003+
tracing::info!(
2004+
"Distances to contract - gateway: {}, peer-a: {}, peer-c: {}",
2005+
gateway_distance.as_f64(),
2006+
peer_a_distance.as_f64(),
2007+
peer_c_distance.as_f64()
2008+
);
2009+
19482010
// Connect to peer A's WebSocket API
19492011
let uri_a = format!(
19502012
"ws://127.0.0.1:{}/v1/contract/command?encodingProtocol=native",

crates/freenet-macros/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,68 @@ async fn test_multi_gateway(ctx: &mut TestContext) -> TestResult {
8282
}
8383
```
8484

85+
#### `node_locations`
86+
Set explicit ring locations for nodes (order must match the `nodes` list). This is useful for deterministic routing topologies in multi-hop tests.
87+
88+
```rust
89+
#[freenet_test(
90+
nodes = ["gateway", "peer-a", "peer-b"],
91+
node_locations = [0.2, 0.6, 0.85], // gateway, peer-a, peer-b
92+
auto_connect_peers = true
93+
)]
94+
async fn test_with_locations(ctx: &mut TestContext) -> TestResult {
95+
// Test logic here...
96+
Ok(())
97+
}
98+
```
99+
100+
**Rules:**
101+
- Provide one numeric value per node.
102+
- Values should be in the range `[0.0, 1.0]`.
103+
- Omit `node_locations` to use random locations (default behavior).
104+
105+
#### `node_locations_fn`
106+
Provide a function that returns `Vec<f64>` (one per node) when locations need to be computed dynamically (for example, based on a contract’s ring location).
107+
108+
```rust
109+
fn my_locations() -> Vec<f64> {
110+
// Must return one entry per node in the same order.
111+
vec![0.1, 0.6, 0.9]
112+
}
113+
114+
#[freenet_test(
115+
nodes = ["gateway", "peer-a", "peer-b"],
116+
node_locations_fn = my_locations,
117+
auto_connect_peers = true
118+
)]
119+
```
120+
121+
- The function must return exactly as many values as there are nodes; otherwise the test fails early.
122+
- `node_locations` and `node_locations_fn` are mutually exclusive.
123+
124+
#### `node_configs`
125+
Override configuration for specific nodes using a map (order must still match the `nodes` list for implicit behavior). Currently supports setting explicit locations per node, with additional fields planned as the macro evolves.
126+
127+
```rust
128+
fn my_gateway_location() -> f64 { 0.25 }
129+
130+
#[freenet_test(
131+
nodes = ["gateway", "peer-a", "peer-b"],
132+
node_configs = {
133+
"gateway": { location: my_gateway_location() },
134+
"peer-a": { location: 0.55 },
135+
"peer-b": { location: 0.90 },
136+
}
137+
)]
138+
async fn test_with_node_configs(ctx: &mut TestContext) -> TestResult {
139+
// Test logic ...
140+
Ok(())
141+
}
142+
```
143+
144+
- Only the fields specified are overridden; unspecified nodes fall back to the default/random configuration.
145+
- Additional fields can be added over time without changing call sites (e.g., `token_ttl`, custom secrets, etc.).
146+
85147
#### `auto_connect_peers`
86148
Automatically configure all peer nodes to connect to all gateway nodes.
87149

crates/freenet-macros/src/codegen.rs

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,83 @@
33
use crate::parser::{AggregateEventsMode, FreenetTestArgs};
44
use proc_macro2::TokenStream;
55
use quote::{format_ident, quote};
6-
use syn::{ItemFn, LitInt, Result};
6+
use syn::{ItemFn, LitInt, LitStr, Result};
7+
8+
/// Get the configured location for a node or fall back to randomness.
9+
fn node_location(args: &FreenetTestArgs, idx: usize, label: &str) -> TokenStream {
10+
if let Some(config) = args.node_configs.get(label) {
11+
if let Some(expr) = &config.location_expr {
12+
let label_lit = LitStr::new(label, proc_macro2::Span::call_site());
13+
return quote! {{
14+
let value: f64 = (#expr);
15+
if !(0.0..=1.0).contains(&value) {
16+
panic!(
17+
"node '{}' location {} is out of range [0.0, 1.0]",
18+
#label_lit, value
19+
);
20+
}
21+
value
22+
}};
23+
}
24+
}
25+
26+
if let Some(ref locations) = args.node_locations {
27+
let value = locations[idx];
28+
return quote! { #value };
29+
}
30+
31+
if args.node_locations_fn.is_some() {
32+
let idx_lit = syn::Index::from(idx);
33+
return quote! {{
34+
if let Some(ref locs) = __node_locations {
35+
locs[#idx_lit]
36+
} else {
37+
rand::Rng::random(&mut rand::rng())
38+
}
39+
}};
40+
}
41+
42+
quote! { rand::Rng::random(&mut rand::rng()) }
43+
}
44+
45+
/// Generate node location initialization (literal list or function).
46+
fn generate_node_locations_init(args: &FreenetTestArgs) -> TokenStream {
47+
let node_count = args.nodes.len();
48+
49+
if let Some(ref fn_path) = args.node_locations_fn {
50+
quote! {
51+
let __node_locations: Option<Vec<f64>> = {
52+
let locs = #fn_path();
53+
if locs.len() != #node_count {
54+
return Err(anyhow::anyhow!(
55+
"node_locations_fn returned {} locations, expected {}",
56+
locs.len(),
57+
#node_count
58+
));
59+
}
60+
for (idx, loc) in locs.iter().enumerate() {
61+
if !(0.0..=1.0).contains(loc) {
62+
return Err(anyhow::anyhow!(
63+
"node_locations_fn value at index {} is out of range: {} (must be in [0.0, 1.0])",
64+
idx,
65+
loc
66+
));
67+
}
68+
}
69+
Some(locs)
70+
};
71+
}
72+
} else if let Some(ref locations) = args.node_locations {
73+
let values: Vec<_> = locations.iter().map(|loc| quote! { #loc }).collect();
74+
quote! {
75+
let __node_locations: Option<Vec<f64>> = Some(vec![#(#values),*]);
76+
}
77+
} else {
78+
quote! {
79+
let __node_locations: Option<Vec<f64>> = None;
80+
}
81+
}
82+
}
783

884
/// Helper to determine if a node is a gateway
985
fn is_gateway(args: &FreenetTestArgs, node_label: &str, node_idx: usize) -> bool {
@@ -27,6 +103,7 @@ pub fn generate_test_code(args: FreenetTestArgs, input_fn: ItemFn) -> Result<Tok
27103

28104
// Generate node setup code
29105
let node_setup = generate_node_setup(&args);
106+
let node_locations_init = generate_node_locations_init(&args);
30107

31108
// Extract values before configs are moved
32109
let value_extraction = generate_value_extraction(&args);
@@ -71,6 +148,7 @@ pub fn generate_test_code(args: FreenetTestArgs, input_fn: ItemFn) -> Result<Tok
71148
tracing::info!("Starting test: {}", stringify!(#test_fn_name));
72149

73150
// 2. Create node configurations
151+
#node_locations_init
74152
#node_setup
75153

76154
// 3. Extract values before configs are moved
@@ -117,6 +195,7 @@ fn generate_node_setup(args: &FreenetTestArgs) -> TokenStream {
117195

118196
if is_gw {
119197
// Gateway node configuration
198+
let location_expr = node_location(args, idx, node_label);
120199
setup_code.push(quote! {
121200
let (#config_var, #temp_var) = {
122201
let temp_dir = tempfile::tempdir()?;
@@ -128,7 +207,7 @@ fn generate_node_setup(args: &FreenetTestArgs) -> TokenStream {
128207
let network_port = freenet::test_utils::reserve_local_port()?;
129208
let ws_port = freenet::test_utils::reserve_local_port()?;
130209

131-
let location: f64 = rand::Rng::random(&mut rand::rng());
210+
let location: f64 = #location_expr;
132211

133212
let config = freenet::config::ConfigArgs {
134213
ws_api: freenet::config::WebsocketApiArgs {
@@ -193,6 +272,7 @@ fn generate_node_setup(args: &FreenetTestArgs) -> TokenStream {
193272
let is_gw = is_gateway(args, node_label, idx);
194273

195274
if !is_gw {
275+
let location_expr = node_location(args, idx, node_label);
196276
// Collect gateway info variables to serialize
197277
let gateways_config = if args.auto_connect_peers {
198278
// Collect all gateway_info_X variables
@@ -238,7 +318,7 @@ fn generate_node_setup(args: &FreenetTestArgs) -> TokenStream {
238318
let network_port = freenet::test_utils::reserve_local_port()?;
239319
let ws_port = freenet::test_utils::reserve_local_port()?;
240320

241-
let location: f64 = rand::Rng::random(&mut rand::rng());
321+
let location: f64 = #location_expr;
242322

243323
let config = freenet::config::ConfigArgs {
244324
ws_api: freenet::config::WebsocketApiArgs {

0 commit comments

Comments
 (0)