Files
Eric Curtin 10af3e609f refactor: deduplicate shared test helpers (#1399)
Extract four categories of copy-pasted test code into single
canonical locations:

- POLICY_OBJECT_TYPE / DRAFT_CHUNK_OBJECT_TYPE constants that were
  defined identically in both persistence/sqlite.rs and
  persistence/postgres.rs are now owned by persistence/mod.rs.

- test_server_state() was duplicated across grpc/provider.rs,
  grpc/policy.rs, and grpc/sandbox.rs. A shared grpc::test_support
  module in grpc/mod.rs now owns the single implementation; each
  submodule imports it.

- The TestOpenShell stub (full OpenShell trait impl), install_rustls_provider(),
  PkiBundle, generate_pki(), and start_test_server() were copied across
  up to five openshell-server integration test files. A new
  tests/common/mod.rs module owns them; each test file uses mod common.

- StubResponse, unique_socket_path(), and spawn_podman_stub() were
  duplicated between openshell-driver-podman/src/driver.rs and
  openshell-driver-podman/src/grpc.rs. A new src/test_utils.rs
  (cfg(test)-gated) owns the shared helpers.
2026-05-19 12:26:12 -07:00

120 lines
4.5 KiB
Rust

// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//! Shared test helpers for openshell-driver-podman unit tests.
use http_body_util::Full;
use hyper::StatusCode;
use hyper::body::Bytes;
use hyper::server::conn::http1;
use hyper::service::service_fn;
use hyper_util::rt::TokioIo;
use std::collections::VecDeque;
use std::convert::Infallible;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::net::UnixListener;
/// A canned HTTP response for the Podman stub server.
#[derive(Clone)]
pub struct StubResponse {
pub status: StatusCode,
pub body: String,
}
impl StubResponse {
pub fn new(status: StatusCode, body: impl Into<String>) -> Self {
Self {
status,
body: body.into(),
}
}
}
/// Generate a unique Unix socket path for a test.
///
/// Uses the current PID and nanosecond timestamp to avoid collisions between
/// concurrent test runs.
pub fn unique_socket_path(test_name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be after unix epoch")
.as_nanos();
PathBuf::from(format!(
"/tmp/openshell-podman-{test_name}-{}-{nanos}.sock",
std::process::id()
))
}
/// Spawn a Unix-socket HTTP stub that serves the given `responses` in order.
///
/// Returns:
/// - the socket path (already bound and listening)
/// - a shared log of `"METHOD /path"` strings, one per request received
/// - a join handle that resolves once all expected requests have been served
pub fn spawn_podman_stub(
test_name: &str,
responses: Vec<StubResponse>,
) -> (
PathBuf,
Arc<Mutex<Vec<String>>>,
tokio::task::JoinHandle<()>,
) {
let socket_path = unique_socket_path(test_name);
let _ = std::fs::remove_file(&socket_path);
let listener = UnixListener::bind(&socket_path).expect("test socket should bind");
let request_log = Arc::new(Mutex::new(Vec::new()));
let response_queue = Arc::new(Mutex::new(VecDeque::from(responses)));
let expected = response_queue
.lock()
.expect("response queue lock should not be poisoned")
.len();
let socket_path_for_task = socket_path.clone();
let log_for_task = request_log.clone();
let queue_for_task = response_queue;
let handle = tokio::spawn(async move {
for _ in 0..expected {
let (stream, _) = listener.accept().await.expect("test stub should accept");
let log = log_for_task.clone();
let queue = queue_for_task.clone();
let result = http1::Builder::new()
.serve_connection(
TokioIo::new(stream),
service_fn(move |req| {
let log = log.clone();
let queue = queue.clone();
async move {
let path = req.uri().path_and_query().map_or_else(
|| req.uri().path().to_string(),
|pq| pq.as_str().to_string(),
);
log.lock()
.expect("request log lock should not be poisoned")
.push(format!("{} {}", req.method(), path));
let response = queue
.lock()
.expect("response queue lock should not be poisoned")
.pop_front()
.expect("stub response should exist");
Ok::<_, Infallible>(
hyper::Response::builder()
.status(response.status)
.body(Full::new(Bytes::from(response.body)))
.expect("stub response should build"),
)
}
}),
)
.await;
// The one-shot test client can close the Unix socket after the
// response, which Hyper reports as a shutdown error. Let the
// request log assertions below decide whether the stub served
// the expected API calls.
let _ = result;
}
let _ = std::fs::remove_file(&socket_path_for_task);
});
(socket_path, request_log, handle)
}