spawn-lnd 0.2.0

Docker-backed Bitcoin Core and LND regtest clusters for Rust integration tests
Documentation
use spawn_lnd::{DockerClient, SpawnError, SpawnLnd};
use std::collections::HashSet;

#[tokio::test]
async fn startup_failure_removes_partially_created_containers()
-> Result<(), Box<dyn std::error::Error>> {
    if std::env::var("RUN_DOCKER_TESTS").as_deref() != Ok("1") {
        eprintln!("skipping startup failure smoke test; set RUN_DOCKER_TESTS=1 to run it");
        return Ok(());
    }

    eprintln!("startup failure smoke: connecting to Docker");
    let docker = DockerClient::connect().await?;
    eprintln!("startup failure smoke: snapshotting managed resources before failure");
    let containers_before = docker
        .managed_container_ids()
        .await?
        .into_iter()
        .collect::<HashSet<_>>();
    let networks_before = docker
        .managed_network_ids()
        .await?
        .into_iter()
        .collect::<HashSet<_>>();
    eprintln!(
        "startup failure smoke: managed containers before={} networks before={}",
        containers_before.len(),
        networks_before.len()
    );

    eprintln!("startup failure smoke: spawning cluster with missing LND image");
    let result = SpawnLnd::builder()
        .node("alice")
        .lnd_image("spawn-lnd/missing-lnd-image:missing")
        .spawn()
        .await;
    eprintln!(
        "startup failure smoke: spawn result is_err={}",
        result.is_err()
    );

    assert!(result.is_err(), "invalid LND image should fail startup");
    assert!(
        matches!(result, Err(SpawnError::Lnd { alias, .. }) if alias == "alice"),
        "expected LND image failure after bitcoind startup"
    );

    let leaked = docker
        .managed_container_ids()
        .await?
        .into_iter()
        .filter(|id| !containers_before.contains(id))
        .collect::<Vec<_>>();
    let leaked_networks = docker
        .managed_network_ids()
        .await?
        .into_iter()
        .filter(|id| !networks_before.contains(id))
        .collect::<Vec<_>>();
    eprintln!("startup failure smoke: leaked managed containers after failed spawn={leaked:?}");
    eprintln!(
        "startup failure smoke: leaked managed networks after failed spawn={leaked_networks:?}"
    );
    if !leaked.is_empty() {
        eprintln!("startup failure smoke: rolling back leaked containers");
        let _ = docker.rollback_containers(leaked.clone()).await;
    }

    assert!(
        leaked.is_empty(),
        "failed cluster startup leaked managed containers: {leaked:?}"
    );
    assert!(
        leaked_networks.is_empty(),
        "failed cluster startup leaked managed networks: {leaked_networks:?}"
    );

    Ok(())
}