use std::path::PathBuf;
use std::process::Command;
use std::sync::OnceLock;
use std::time::Duration;
use anyhow::Context;
use freenet::test_utils::{self, TestContext, make_get};
use freenet_macros::freenet_test;
use freenet_stdlib::client_api::{ContractResponse, HostResponse, WebApi};
use freenet_stdlib::prelude::*;
use testresult::TestResult;
use tokio_tungstenite::connect_async;
const TEST_CONTRACT: &str = "test-contract-integration";
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("workspace layout: crates/core/../../ should resolve")
.to_path_buf()
}
fn target_dir() -> PathBuf {
std::env::var_os("CARGO_TARGET_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| workspace_root().join("target"))
}
fn fdev_bin() -> PathBuf {
let mut path = target_dir().join("debug").join("fdev");
if !path.exists() {
let release = target_dir().join("release").join("fdev");
if release.exists() {
path = release;
}
}
assert!(
path.exists(),
"fdev binary not found at {path:?}. Build it first: \
`cargo build --bin fdev` (CI's test_unit job does this in \
its Build step before the Test step)."
);
path
}
fn packaged_contract_path() -> PathBuf {
static CACHE: OnceLock<PathBuf> = OnceLock::new();
CACHE
.get_or_init(|| {
let crate_dir = workspace_root().join("tests").join(TEST_CONTRACT);
assert!(
crate_dir.exists(),
"test contract crate missing at {crate_dir:?}"
);
let status = Command::new(fdev_bin())
.arg("build")
.current_dir(&crate_dir)
.status()
.expect("spawn fdev build");
assert!(status.success(), "fdev build failed for {TEST_CONTRACT}");
let path = crate_dir
.join("build")
.join("freenet")
.join(TEST_CONTRACT.replace('-', "_"));
assert!(
path.exists(),
"fdev build did not produce expected artifact at {path:?}"
);
path
})
.clone()
}
fn write_raw_wasm_into(dst: &std::path::Path) -> anyhow::Result<()> {
const HEADER_LEN: usize = 8 + 32;
const WASM_MAGIC: &[u8; 4] = b"\0asm";
let bytes = std::fs::read(packaged_contract_path()).context("read packaged contract")?;
anyhow::ensure!(
bytes.len() > HEADER_LEN,
"packaged file shorter than 40-byte header: {}",
bytes.len()
);
let raw = &bytes[HEADER_LEN..];
anyhow::ensure!(
raw.starts_with(WASM_MAGIC),
"after stripping {HEADER_LEN}-byte header, expected WASM magic, got: {:02x?}",
&raw[..raw.len().min(8)]
);
std::fs::write(dst, raw).context("write raw wasm")?;
Ok(())
}
fn write_initial_state_into(dst: &std::path::Path) -> anyhow::Result<()> {
let bytes = test_utils::create_empty_todo_list();
std::fs::write(dst, bytes).context("write initial state")?;
Ok(())
}
fn fdev_run(args: &[&str]) -> String {
let output = Command::new(fdev_bin())
.args(args)
.output()
.expect("spawn fdev");
if !output.status.success() {
panic!(
"fdev {args:?} exited {:?}\n--- stdout ---\n{}\n--- stderr ---\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
}
String::from_utf8(output.stdout).expect("fdev stdout not utf-8")
}
fn fdev_run_expect_failure(args: &[&str]) -> (Option<i32>, String) {
let output = Command::new(fdev_bin())
.args(args)
.output()
.expect("spawn fdev");
assert!(
!output.status.success(),
"fdev {args:?} unexpectedly succeeded\nstdout: {}",
String::from_utf8_lossy(&output.stdout),
);
(
output.status.code(),
String::from_utf8_lossy(&output.stderr).to_string(),
)
}
async fn fdev_publish_observed(args: &[&str]) {
let output = Command::new(fdev_bin())
.args(args)
.output()
.expect("spawn fdev");
if output.status.success() {
tracing::info!(
"fdev publish exited 0: {}",
String::from_utf8_lossy(&output.stdout).trim_end()
);
} else {
tracing::warn!(
"fdev publish reported timeout/error (expected race with \
fixture's Put driver): exit={:?}\nstderr: {}",
output.status.code(),
String::from_utf8_lossy(&output.stderr).trim_end(),
);
}
}
async fn wait_for_contract_via_get(
ws_url: &str,
key: ContractKey,
within: Duration,
) -> anyhow::Result<bool> {
let deadline = std::time::Instant::now() + within;
let (stream, _) = connect_async(ws_url)
.await
.with_context(|| format!("connect to peer ws {ws_url}"))?;
let mut client = WebApi::start(stream);
const ATTEMPT_TIMEOUT: Duration = Duration::from_secs(15);
while std::time::Instant::now() < deadline {
make_get(&mut client, key, true, false).await?;
match tokio::time::timeout(ATTEMPT_TIMEOUT, client.recv()).await {
Ok(Ok(HostResponse::ContractResponse(ContractResponse::GetResponse {
contract: Some(_),
..
}))) => return Ok(true),
Ok(Ok(HostResponse::ContractResponse(ContractResponse::GetResponse {
contract: None,
..
}))) => {
tokio::time::sleep(Duration::from_secs(1)).await;
}
Ok(Ok(other)) => {
tracing::warn!("unexpected response while waiting for Get: {other:?}");
tokio::time::sleep(Duration::from_secs(1)).await;
}
Ok(Err(e)) => {
tracing::warn!("Get attempt errored: {e}");
tokio::time::sleep(Duration::from_secs(1)).await;
}
Err(_) => {
tracing::warn!("Get attempt timed out, retrying");
}
}
}
Ok(false)
}
#[test]
fn get_contract_id_matches_between_raw_and_packaged() -> TestResult {
let tmp = tempfile::tempdir()?;
let raw_path = tmp.path().join("raw.wasm");
write_raw_wasm_into(&raw_path)?;
let packaged_path = packaged_contract_path();
let id_packaged = fdev_run(&["get-contract-id", "--code", packaged_path.to_str().unwrap()])
.trim()
.to_string();
let id_raw = fdev_run(&["get-contract-id", "--code", raw_path.to_str().unwrap()])
.trim()
.to_string();
assert!(!id_packaged.is_empty(), "fdev printed empty contract id");
assert_eq!(
id_packaged, id_raw,
"raw and packaged inputs must hash to the same contract id \
(pre-fix the packaged path folded the 40-byte header into \
ContractCode.data → distinct ids)"
);
Ok(())
}
#[test]
fn get_contract_id_rejects_garbage_and_empty() -> TestResult {
let tmp = tempfile::tempdir()?;
for (name, bytes) in [
("empty.bin", &[][..]),
("garbage.bin", &[0xff; 16][..]),
("near-magic.bin", &[0x00, 0x61, 0x73, 0x99][..]),
] {
let path = tmp.path().join(name);
std::fs::write(&path, bytes)?;
let (exit, stderr) =
fdev_run_expect_failure(&["get-contract-id", "--code", path.to_str().unwrap()]);
assert!(
!stderr.is_empty() || exit != Some(0),
"{name}: fdev failed but produced no diagnostic"
);
}
Ok(())
}
#[freenet_test(
health_check_readiness = true,
nodes = ["gateway", "peer-a"],
// Matches the known-working fixture in operations.rs::test_put_contract:
// 300s budget + 4 worker threads. With fewer workers / shorter wait the
// first Put attempt sometimes hits the node's single-shot timeout while
// the network topology is still settling.
timeout_secs = 300,
startup_wait_secs = 30,
tokio_flavor = "multi_thread",
tokio_worker_threads = 4,
)]
async fn publish_packaged_contract_round_trip(ctx: &mut TestContext) -> TestResult {
let peer = ctx.node("peer-a")?;
let tmp = tempfile::tempdir()?;
let state_path = tmp.path().join("state.json");
write_initial_state_into(&state_path)?;
let packaged = packaged_contract_path();
let container = test_utils::load_contract(TEST_CONTRACT, vec![].into())?;
let expected_key = container.key();
let url = peer.ws_url();
fdev_publish_observed(&[
"--node-url",
&url,
"publish",
"--code",
packaged.to_str().unwrap(),
"contract",
"--state",
state_path.to_str().unwrap(),
])
.await;
assert!(
wait_for_contract_via_get(&url, expected_key, Duration::from_secs(120)).await?,
"packaged contract not found via Get on peer-a for key {} \
— did wasmtime reject the bytes? (#4075 regression: pre-fix \
the packaged path included the 40-byte header in the bytes \
shipped to the node, wasmtime rejected with \"compile: input \
bytes aren't valid utf-8\", and the contract never reached \
the store)",
expected_key,
);
Ok(())
}
#[freenet_test(
health_check_readiness = true,
nodes = ["gateway", "peer-a"],
// Matches the known-working fixture in operations.rs::test_put_contract:
// 300s budget + 4 worker threads. With fewer workers / shorter wait the
// first Put attempt sometimes hits the node's single-shot timeout while
// the network topology is still settling.
timeout_secs = 300,
startup_wait_secs = 30,
tokio_flavor = "multi_thread",
tokio_worker_threads = 4,
)]
async fn publish_raw_wasm_round_trip(ctx: &mut TestContext) -> TestResult {
let peer = ctx.node("peer-a")?;
let tmp = tempfile::tempdir()?;
let raw_path = tmp.path().join("raw.wasm");
write_raw_wasm_into(&raw_path)?;
let state_path = tmp.path().join("state.json");
write_initial_state_into(&state_path)?;
let container = test_utils::load_contract(TEST_CONTRACT, vec![].into())?;
let expected_key = container.key();
let url = peer.ws_url();
fdev_publish_observed(&[
"--node-url",
&url,
"publish",
"--code",
raw_path.to_str().unwrap(),
"contract",
"--state",
state_path.to_str().unwrap(),
])
.await;
assert!(
wait_for_contract_via_get(&url, expected_key, Duration::from_secs(120)).await?,
"raw WASM contract not found via Get on peer-a for key {} \
(regression on the previously-working raw-input path)",
expected_key,
);
Ok(())
}