From 5a0c302a137951b0b9891c79ba67306bff5fcd6a Mon Sep 17 00:00:00 2001 From: Dodothereal <129273127+Dodothereal@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:42:42 +0100 Subject: [PATCH 1/2] test: let Cassandra suites use a configurable contact point Read EXTENDDB_TEST_CASSANDRA_CONTACT_POINT in both the shared test config and the skip probe, retaining 127.0.0.1:9042 by default and bracketing an unbracketed IPv6 host. Test the pure resolver so parallel tests never observe a changed environment, and mention the variable in the runner. Signed-off-by: Dodothereal <129273127+Dodothereal@users.noreply.github.com> --- crates/storage-cassandra/tests/common/mod.rs | 60 +++++++++++++++++--- devtools/run-cassandra-tests | 4 +- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/crates/storage-cassandra/tests/common/mod.rs b/crates/storage-cassandra/tests/common/mod.rs index c1fd1e971..b26ccdf42 100644 --- a/crates/storage-cassandra/tests/common/mod.rs +++ b/crates/storage-cassandra/tests/common/mod.rs @@ -12,6 +12,7 @@ use extenddb_storage::TableEngine; use extenddb_storage_cassandra::{ CassandraCatalogStore, CassandraEngine, CassandraSession, CassandraStorageConfig, }; +use std::net::ToSocketAddrs; use std::sync::Arc; /// Keyspaces queued for async drop between tests. @@ -46,10 +47,32 @@ pub async fn flush_pending_drops(session: &Arc) { let _ = session.query(format!("DROP KEYSPACE IF EXISTS {ks}")).await; } } +const DEFAULT_TEST_CONTACT_POINT: &str = "127.0.0.1:9042"; +const TEST_CONTACT_POINT_ENV: &str = "EXTENDDB_TEST_CASSANDRA_CONTACT_POINT"; + +fn test_contact_point() -> String { + resolve_test_contact_point(std::env::var(TEST_CONTACT_POINT_ENV).ok().as_deref()) +} + +fn resolve_test_contact_point(value: Option<&str>) -> String { + let contact_point = value + .map(str::trim) + .filter(|value| !value.is_empty()) + .unwrap_or(DEFAULT_TEST_CONTACT_POINT) + .to_string(); + let Some((host, port)) = contact_point.rsplit_once(':') else { + return contact_point; + }; + if host.contains(':') && !host.starts_with('[') { + return format!("[{host}]:{port}"); + } + contact_point +} + /// Returns a standard test configuration for Cassandra. pub fn test_config() -> CassandraStorageConfig { let mut config = CassandraStorageConfig { - contact_points: vec!["127.0.0.1:9042".to_string()], + contact_points: vec![test_contact_point()], username: Some("cassandra".to_string()), password: Some("cassandra".to_string()), keyspace_prefix: "extenddb_ttl_test".to_string(), @@ -714,8 +737,10 @@ pub async fn put_item_then_lock( /// True when the suite should be skipped because no Cassandra is reachable. /// +/// `EXTENDDB_TEST_CASSANDRA_CONTACT_POINT` selects the node (default +/// 127.0.0.1:9042), so parallel jobs on one host can use separate instances. /// The workspace `cargo test` job in test.yml runs with no services, so these -/// suites probe 127.0.0.1:9042 and skip themselves when nothing is listening. +/// suites probe that node and skip themselves when nothing is listening. /// The dedicated integration-cassandra workflow provides a service container /// and sets `EXTENDDB_TEST_CASSANDRA=required`, under which this never skips: /// if Cassandra is down there, setup fails loudly rather than the suite @@ -725,15 +750,34 @@ pub fn skip_without_cassandra() -> bool { if std::env::var("EXTENDDB_TEST_CASSANDRA").as_deref() == Ok("required") { return false; } - let reachable = std::net::TcpStream::connect_timeout( - &std::net::SocketAddr::from(([127, 0, 0, 1], 9042)), - std::time::Duration::from_secs(2), - ) - .is_ok(); + let contact_point = test_contact_point(); + let reachable = contact_point + .to_socket_addrs() + .ok() + .and_then(|mut addrs| addrs.next()) + .is_some_and(|addr| { + std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_secs(2)).is_ok() + }); if !reachable { eprintln!( - "skipping: no Cassandra at 127.0.0.1:9042 (set EXTENDDB_TEST_CASSANDRA=required to fail instead)" + "skipping: no Cassandra at {contact_point} (set EXTENDDB_TEST_CASSANDRA=required to fail instead)" ); } !reachable } + +#[cfg(test)] +mod contact_point_tests { + use super::resolve_test_contact_point; + + #[test] + fn contact_point_defaults_and_honours_an_override() { + assert_eq!(resolve_test_contact_point(None), "127.0.0.1:9042"); + assert_eq!( + resolve_test_contact_point(Some("127.0.0.1:19042")), + "127.0.0.1:19042" + ); + assert_eq!(resolve_test_contact_point(Some("::1:19042")), "[::1]:19042"); + assert_eq!(resolve_test_contact_point(Some(" ")), "127.0.0.1:9042"); + } +} diff --git a/devtools/run-cassandra-tests b/devtools/run-cassandra-tests index dcc67a8b6..77c9d3853 100755 --- a/devtools/run-cassandra-tests +++ b/devtools/run-cassandra-tests @@ -29,7 +29,9 @@ # # Prerequisites: # - A Cassandra node reachable at the contact point (default 127.0.0.1:9042) -# with PasswordAuthenticator enabled. The official cassandra:4.1 image does +# with PasswordAuthenticator enabled. The direct Rust test harness reads +# EXTENDDB_TEST_CASSANDRA_CONTACT_POINT when it is set; the --contact-points +# option below configures the extenddb server and Python suite. The official cassandra:4.1 image does # not honour CASSANDRA_AUTHENTICATOR, so patch it after starting: # docker run -d --name cassandra -p 9042:9042 \ # -e CASSANDRA_CLUSTER_NAME=extenddb-ci \ From 02994703022ac82767c72b91199570d82994fd3d Mon Sep 17 00:00:00 2001 From: Giorgio Maria Federico Birnthaler <129273127+Dodothereal@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:27:40 +0100 Subject: [PATCH 2/2] test: address Cassandra contact-point review feedback Probe every resolved address, leave nonblank contact points unchanged, and cover configuration forwarding and loopback reachability. Correct the runner help range and integration-suite contact-point documentation. Signed-off-by: Dodothereal <129273127+Dodothereal@users.noreply.github.com> --- crates/storage-cassandra/tests/common/mod.rs | 81 ++++++++++++++----- .../tests/direct_integration.rs | 5 +- .../tests/ttl_integration.rs | 5 +- devtools/run-cassandra-tests | 10 +-- 4 files changed, 73 insertions(+), 28 deletions(-) diff --git a/crates/storage-cassandra/tests/common/mod.rs b/crates/storage-cassandra/tests/common/mod.rs index b26ccdf42..b1e6de63c 100644 --- a/crates/storage-cassandra/tests/common/mod.rs +++ b/crates/storage-cassandra/tests/common/mod.rs @@ -55,24 +55,21 @@ fn test_contact_point() -> String { } fn resolve_test_contact_point(value: Option<&str>) -> String { - let contact_point = value + value .map(str::trim) .filter(|value| !value.is_empty()) .unwrap_or(DEFAULT_TEST_CONTACT_POINT) - .to_string(); - let Some((host, port)) = contact_point.rsplit_once(':') else { - return contact_point; - }; - if host.contains(':') && !host.starts_with('[') { - return format!("[{host}]:{port}"); - } - contact_point + .to_string() } /// Returns a standard test configuration for Cassandra. pub fn test_config() -> CassandraStorageConfig { + config_for(test_contact_point()) +} + +fn config_for(contact_point: String) -> CassandraStorageConfig { let mut config = CassandraStorageConfig { - contact_points: vec![test_contact_point()], + contact_points: vec![contact_point], username: Some("cassandra".to_string()), password: Some("cassandra".to_string()), keyspace_prefix: "extenddb_ttl_test".to_string(), @@ -751,13 +748,7 @@ pub fn skip_without_cassandra() -> bool { return false; } let contact_point = test_contact_point(); - let reachable = contact_point - .to_socket_addrs() - .ok() - .and_then(|mut addrs| addrs.next()) - .is_some_and(|addr| { - std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_secs(2)).is_ok() - }); + let reachable = node_reachable(contact_point.as_str()); if !reachable { eprintln!( "skipping: no Cassandra at {contact_point} (set EXTENDDB_TEST_CASSANDRA=required to fail instead)" @@ -766,9 +757,18 @@ pub fn skip_without_cassandra() -> bool { !reachable } +fn node_reachable(contact_point: impl ToSocketAddrs) -> bool { + contact_point.to_socket_addrs().is_ok_and(|mut addrs| { + addrs.any(|addr| { + std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_secs(2)).is_ok() + }) + }) +} + #[cfg(test)] mod contact_point_tests { - use super::resolve_test_contact_point; + use super::{config_for, node_reachable, resolve_test_contact_point}; + use std::net::TcpListener; #[test] fn contact_point_defaults_and_honours_an_override() { @@ -777,7 +777,50 @@ mod contact_point_tests { resolve_test_contact_point(Some("127.0.0.1:19042")), "127.0.0.1:19042" ); - assert_eq!(resolve_test_contact_point(Some("::1:19042")), "[::1]:19042"); + assert_eq!( + resolve_test_contact_point(Some(" 127.0.0.1:19042 ")), + "127.0.0.1:19042" + ); + assert_eq!(resolve_test_contact_point(Some("")), "127.0.0.1:9042"); assert_eq!(resolve_test_contact_point(Some(" ")), "127.0.0.1:9042"); } + + #[test] + fn contact_point_does_not_rewrite_nonblank_values() { + for value in ["::1", "[::1]:19042", "127.0.0.1:19042,127.0.0.1:19043"] { + assert_eq!(resolve_test_contact_point(Some(value)), value); + } + } + + #[test] + fn config_keeps_the_selected_contact_point() { + let config = config_for("127.0.0.1:19042".to_string()); + assert_eq!(config.contact_points, vec!["127.0.0.1:19042"]); + } + + #[test] + fn probe_uses_the_selected_contact_point() -> std::io::Result<()> { + let listener = TcpListener::bind("127.0.0.1:0")?; + let contact_point = listener.local_addr()?.to_string(); + assert!(node_reachable(contact_point.as_str())); + drop(listener); + assert!(!node_reachable(contact_point.as_str())); + Ok(()) + } + + #[test] + fn probe_tries_later_resolved_addresses() -> std::io::Result<()> { + let first = TcpListener::bind("127.0.0.1:0")?; + let second = TcpListener::bind("127.0.0.1:0")?; + let addresses = [first.local_addr()?, second.local_addr()?]; + drop(first); + assert!(!node_reachable(addresses[0])); + assert!(node_reachable(&addresses[..])); + Ok(()) + } + + #[test] + fn probe_rejects_an_invalid_contact_point() { + assert!(!node_reachable("127.0.0.1:not-a-port")); + } } diff --git a/crates/storage-cassandra/tests/direct_integration.rs b/crates/storage-cassandra/tests/direct_integration.rs index 43eb28b81..a9bfe3137 100644 --- a/crates/storage-cassandra/tests/direct_integration.rs +++ b/crates/storage-cassandra/tests/direct_integration.rs @@ -4,8 +4,9 @@ //! Direct storage-trait integration tests, ported from the original //! extenddb-cassandra-plugin repository (its tests/rust suite). They exercise //! the Cassandra engine's trait implementations directly against a live node -//! at 127.0.0.1:9042 — no HTTP server in between — and skip themselves when -//! no Cassandra is reachable (see helpers::skip_without_cassandra). +//! at the contact point in `EXTENDDB_TEST_CASSANDRA_CONTACT_POINT` (default +//! 127.0.0.1:9042) — no HTTP server in between — and skip themselves when no +//! Cassandra is reachable (see helpers::skip_without_cassandra). //! //! One binary, one module per area, sharing tests/common/mod.rs, which is the //! in-tree descendant of the plug-in repo's helpers.rs. diff --git a/crates/storage-cassandra/tests/ttl_integration.rs b/crates/storage-cassandra/tests/ttl_integration.rs index 279d79492..22da810c4 100644 --- a/crates/storage-cassandra/tests/ttl_integration.rs +++ b/crates/storage-cassandra/tests/ttl_integration.rs @@ -3,8 +3,9 @@ //! Integration tests for Cassandra TTL against a live Cassandra. //! -//! Requires Cassandra on 127.0.0.1:9042. There is no Cassandra CI workflow, so -//! these are run locally. +//! Requires Cassandra at the contact point in +//! `EXTENDDB_TEST_CASSANDRA_CONTACT_POINT` (default 127.0.0.1:9042). The +//! integration-cassandra workflow runs these against a service container. #[path = "common/mod.rs"] mod helpers; diff --git a/devtools/run-cassandra-tests b/devtools/run-cassandra-tests index 77c9d3853..507ff576d 100755 --- a/devtools/run-cassandra-tests +++ b/devtools/run-cassandra-tests @@ -29,10 +29,10 @@ # # Prerequisites: # - A Cassandra node reachable at the contact point (default 127.0.0.1:9042) -# with PasswordAuthenticator enabled. The direct Rust test harness reads -# EXTENDDB_TEST_CASSANDRA_CONTACT_POINT when it is set; the --contact-points -# option below configures the extenddb server and Python suite. The official cassandra:4.1 image does -# not honour CASSANDRA_AUTHENTICATOR, so patch it after starting: +# with PasswordAuthenticator enabled. The Rust direct tests read +# EXTENDDB_TEST_CASSANDRA_CONTACT_POINT (default 127.0.0.1:9042), which +# --contact-points above doesn't set. The official cassandra:4.1 image +# does not honour CASSANDRA_AUTHENTICATOR, so patch it after starting: # docker run -d --name cassandra -p 9042:9042 \ # -e CASSANDRA_CLUSTER_NAME=extenddb-ci \ # -e HEAP_NEWSIZE=128M -e MAX_HEAP_SIZE=1024M \ @@ -69,7 +69,7 @@ while [[ $# -gt 0 ]]; do --output) OUTPUT_DIR="$2"; shift 2 ;; --keep) KEEP=true; shift ;; --) shift; RUN_TESTS_ARGS=("$@"); break ;; - -h|--help) sed -n '5,35p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h|--help) sed -n '5,47p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) echo "error: unknown option: $1" >&2; exit 1 ;; esac done