Skip to content

Commit bd83cf0

Browse files
committed
refactor: deduplicate shared test helpers
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.
1 parent d255cdd commit bd83cf0

17 files changed

Lines changed: 775 additions & 2626 deletions

File tree

crates/openshell-driver-podman/src/driver.rs

Lines changed: 2 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -528,18 +528,9 @@ fn check_subuid_range() {
528528
#[cfg(test)]
529529
mod tests {
530530
use super::*;
531-
use http_body_util::Full;
532-
use hyper::body::Bytes;
533-
use hyper::server::conn::http1;
534-
use hyper::service::service_fn;
535-
use hyper::{Response, StatusCode};
536-
use hyper_util::rt::TokioIo;
537-
use std::collections::VecDeque;
538-
use std::convert::Infallible;
531+
use crate::test_utils::{StubResponse, spawn_podman_stub};
532+
use hyper::StatusCode;
539533
use std::path::PathBuf;
540-
use std::sync::{Arc, Mutex};
541-
use std::time::{SystemTime, UNIX_EPOCH};
542-
use tokio::net::UnixListener;
543534

544535
#[test]
545536
fn podman_driver_error_from_conflict() {
@@ -625,32 +616,6 @@ mod tests {
625616
assert_eq!(cfg.grpc_endpoint, "https://gateway.internal:9000");
626617
}
627618

628-
#[derive(Clone)]
629-
struct StubResponse {
630-
status: StatusCode,
631-
body: String,
632-
}
633-
634-
impl StubResponse {
635-
fn new(status: StatusCode, body: impl Into<String>) -> Self {
636-
Self {
637-
status,
638-
body: body.into(),
639-
}
640-
}
641-
}
642-
643-
fn unique_socket_path(test_name: &str) -> PathBuf {
644-
let nanos = SystemTime::now()
645-
.duration_since(UNIX_EPOCH)
646-
.expect("clock should be after unix epoch")
647-
.as_nanos();
648-
PathBuf::from(format!(
649-
"/tmp/openshell-podman-{test_name}-{}-{nanos}.sock",
650-
std::process::id()
651-
))
652-
}
653-
654619
fn test_driver(socket_path: PathBuf) -> PodmanComputeDriver {
655620
let config = PodmanComputeConfig {
656621
socket_path,
@@ -664,71 +629,6 @@ mod tests {
664629
format!("/v5.0.0{path}")
665630
}
666631

667-
fn spawn_podman_stub(
668-
test_name: &str,
669-
responses: Vec<StubResponse>,
670-
) -> (
671-
PathBuf,
672-
Arc<Mutex<Vec<String>>>,
673-
tokio::task::JoinHandle<()>,
674-
) {
675-
let socket_path = unique_socket_path(test_name);
676-
let _ = std::fs::remove_file(&socket_path);
677-
let listener = UnixListener::bind(&socket_path).expect("test socket should bind");
678-
let request_log = Arc::new(Mutex::new(Vec::new()));
679-
let response_queue = Arc::new(Mutex::new(VecDeque::from(responses)));
680-
let expected = response_queue
681-
.lock()
682-
.expect("response queue lock should not be poisoned")
683-
.len();
684-
let socket_path_for_task = socket_path.clone();
685-
let log_for_task = request_log.clone();
686-
let queue_for_task = response_queue;
687-
let handle = tokio::spawn(async move {
688-
for _ in 0..expected {
689-
let (stream, _) = listener.accept().await.expect("test stub should accept");
690-
let log = log_for_task.clone();
691-
let queue = queue_for_task.clone();
692-
let result = http1::Builder::new()
693-
.serve_connection(
694-
TokioIo::new(stream),
695-
service_fn(move |req| {
696-
let log = log.clone();
697-
let queue = queue.clone();
698-
async move {
699-
let path = req.uri().path_and_query().map_or_else(
700-
|| req.uri().path().to_string(),
701-
|pq| pq.as_str().to_string(),
702-
);
703-
log.lock()
704-
.expect("request log lock should not be poisoned")
705-
.push(format!("{} {}", req.method(), path));
706-
let response = queue
707-
.lock()
708-
.expect("response queue lock should not be poisoned")
709-
.pop_front()
710-
.expect("stub response should exist");
711-
Ok::<_, Infallible>(
712-
Response::builder()
713-
.status(response.status)
714-
.body(Full::new(Bytes::from(response.body)))
715-
.expect("stub response should build"),
716-
)
717-
}
718-
}),
719-
)
720-
.await;
721-
// The one-shot test client can close the Unix socket after the
722-
// response, which Hyper reports as a shutdown error. Let the
723-
// request log assertions below decide whether the stub served
724-
// the expected API calls.
725-
let _ = result;
726-
}
727-
let _ = std::fs::remove_file(&socket_path_for_task);
728-
});
729-
(socket_path, request_log, handle)
730-
}
731-
732632
#[tokio::test]
733633
async fn delete_sandbox_cleans_up_with_request_id_when_container_is_already_gone() {
734634
let sandbox_id = "sandbox-123";

crates/openshell-driver-podman/src/grpc.rs

Lines changed: 2 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -156,18 +156,10 @@ mod tests {
156156
use super::*;
157157
use crate::config::PodmanComputeConfig;
158158
use crate::container;
159-
use http_body_util::Full;
160-
use hyper::body::Bytes;
161-
use hyper::server::conn::http1;
162-
use hyper::service::service_fn;
163-
use hyper::{Response as HyperResponse, StatusCode};
164-
use hyper_util::rt::TokioIo;
159+
use crate::test_utils::{StubResponse, spawn_podman_stub, unique_socket_path};
160+
use hyper::StatusCode;
165161
use openshell_core::ComputeDriverError;
166-
use std::collections::VecDeque;
167-
use std::convert::Infallible;
168162
use std::path::PathBuf;
169-
use std::sync::{Arc, Mutex};
170-
use std::time::{SystemTime, UNIX_EPOCH};
171163

172164
#[test]
173165
fn precondition_driver_errors_map_to_failed_precondition_status() {
@@ -184,98 +176,6 @@ mod tests {
184176
assert_eq!(status.code(), tonic::Code::AlreadyExists);
185177
}
186178

187-
#[derive(Clone)]
188-
struct StubResponse {
189-
status: StatusCode,
190-
body: String,
191-
}
192-
193-
impl StubResponse {
194-
fn new(status: StatusCode, body: impl Into<String>) -> Self {
195-
Self {
196-
status,
197-
body: body.into(),
198-
}
199-
}
200-
}
201-
202-
fn unique_socket_path(test_name: &str) -> PathBuf {
203-
let nanos = SystemTime::now()
204-
.duration_since(UNIX_EPOCH)
205-
.expect("clock should be after unix epoch")
206-
.as_nanos();
207-
PathBuf::from(format!(
208-
"/tmp/openshell-podman-grpc-{test_name}-{}-{nanos}.sock",
209-
std::process::id()
210-
))
211-
}
212-
213-
fn spawn_podman_stub(
214-
test_name: &str,
215-
responses: Vec<StubResponse>,
216-
) -> (
217-
PathBuf,
218-
Arc<Mutex<Vec<String>>>,
219-
tokio::task::JoinHandle<()>,
220-
) {
221-
let socket_path = unique_socket_path(test_name);
222-
let _ = std::fs::remove_file(&socket_path);
223-
let listener =
224-
tokio::net::UnixListener::bind(&socket_path).expect("test socket should bind");
225-
let request_log = Arc::new(Mutex::new(Vec::new()));
226-
let response_queue = Arc::new(Mutex::new(VecDeque::from(responses)));
227-
let expected = response_queue
228-
.lock()
229-
.expect("response queue lock should not be poisoned")
230-
.len();
231-
let socket_path_for_task = socket_path.clone();
232-
let log_for_task = request_log.clone();
233-
let queue_for_task = response_queue;
234-
let handle = tokio::spawn(async move {
235-
for _ in 0..expected {
236-
let (stream, _) = listener.accept().await.expect("test stub should accept");
237-
let log = log_for_task.clone();
238-
let queue = queue_for_task.clone();
239-
let result = http1::Builder::new()
240-
.serve_connection(
241-
TokioIo::new(stream),
242-
service_fn(move |req| {
243-
let log = log.clone();
244-
let queue = queue.clone();
245-
async move {
246-
let path = req.uri().path_and_query().map_or_else(
247-
|| req.uri().path().to_string(),
248-
|pq| pq.as_str().to_string(),
249-
);
250-
log.lock()
251-
.expect("request log lock should not be poisoned")
252-
.push(format!("{} {}", req.method(), path));
253-
let response = queue
254-
.lock()
255-
.expect("response queue lock should not be poisoned")
256-
.pop_front()
257-
.expect("stub response should exist");
258-
Ok::<_, Infallible>(
259-
HyperResponse::builder()
260-
.status(response.status)
261-
.body(Full::new(Bytes::from(response.body)))
262-
.expect("stub response should build"),
263-
)
264-
}
265-
}),
266-
)
267-
.await;
268-
// The one-shot test client can close the Unix socket after the
269-
// response, which Hyper reports as a shutdown error. Let the
270-
// request log assertions below decide whether the stub served
271-
// the expected API calls.
272-
let _ = result;
273-
}
274-
let _ = std::fs::remove_file(&socket_path_for_task);
275-
});
276-
(socket_path, request_log, handle)
277-
}
278-
279179
fn test_service(socket_path: PathBuf) -> ComputeDriverService {
280180
let config = PodmanComputeConfig {
281181
socket_path,

crates/openshell-driver-podman/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ pub mod config;
66
pub(crate) mod container;
77
pub mod driver;
88
pub mod grpc;
9+
#[cfg(test)]
10+
pub(crate) mod test_utils;
911
pub(crate) mod watcher;
1012

1113
pub use config::PodmanComputeConfig;
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Shared test helpers for openshell-driver-podman unit tests.
5+
6+
use http_body_util::Full;
7+
use hyper::StatusCode;
8+
use hyper::body::Bytes;
9+
use hyper::server::conn::http1;
10+
use hyper::service::service_fn;
11+
use hyper_util::rt::TokioIo;
12+
use std::collections::VecDeque;
13+
use std::convert::Infallible;
14+
use std::path::PathBuf;
15+
use std::sync::{Arc, Mutex};
16+
use std::time::{SystemTime, UNIX_EPOCH};
17+
use tokio::net::UnixListener;
18+
19+
/// A canned HTTP response for the Podman stub server.
20+
#[derive(Clone)]
21+
pub struct StubResponse {
22+
pub status: StatusCode,
23+
pub body: String,
24+
}
25+
26+
impl StubResponse {
27+
pub fn new(status: StatusCode, body: impl Into<String>) -> Self {
28+
Self {
29+
status,
30+
body: body.into(),
31+
}
32+
}
33+
}
34+
35+
/// Generate a unique Unix socket path for a test.
36+
///
37+
/// Uses the current PID and nanosecond timestamp to avoid collisions between
38+
/// concurrent test runs.
39+
pub fn unique_socket_path(test_name: &str) -> PathBuf {
40+
let nanos = SystemTime::now()
41+
.duration_since(UNIX_EPOCH)
42+
.expect("clock should be after unix epoch")
43+
.as_nanos();
44+
PathBuf::from(format!(
45+
"/tmp/openshell-podman-{test_name}-{}-{nanos}.sock",
46+
std::process::id()
47+
))
48+
}
49+
50+
/// Spawn a Unix-socket HTTP stub that serves the given `responses` in order.
51+
///
52+
/// Returns:
53+
/// - the socket path (already bound and listening)
54+
/// - a shared log of `"METHOD /path"` strings, one per request received
55+
/// - a join handle that resolves once all expected requests have been served
56+
pub fn spawn_podman_stub(
57+
test_name: &str,
58+
responses: Vec<StubResponse>,
59+
) -> (
60+
PathBuf,
61+
Arc<Mutex<Vec<String>>>,
62+
tokio::task::JoinHandle<()>,
63+
) {
64+
let socket_path = unique_socket_path(test_name);
65+
let _ = std::fs::remove_file(&socket_path);
66+
let listener = UnixListener::bind(&socket_path).expect("test socket should bind");
67+
let request_log = Arc::new(Mutex::new(Vec::new()));
68+
let response_queue = Arc::new(Mutex::new(VecDeque::from(responses)));
69+
let expected = response_queue
70+
.lock()
71+
.expect("response queue lock should not be poisoned")
72+
.len();
73+
let socket_path_for_task = socket_path.clone();
74+
let log_for_task = request_log.clone();
75+
let queue_for_task = response_queue;
76+
let handle = tokio::spawn(async move {
77+
for _ in 0..expected {
78+
let (stream, _) = listener.accept().await.expect("test stub should accept");
79+
let log = log_for_task.clone();
80+
let queue = queue_for_task.clone();
81+
let result = http1::Builder::new()
82+
.serve_connection(
83+
TokioIo::new(stream),
84+
service_fn(move |req| {
85+
let log = log.clone();
86+
let queue = queue.clone();
87+
async move {
88+
let path = req.uri().path_and_query().map_or_else(
89+
|| req.uri().path().to_string(),
90+
|pq| pq.as_str().to_string(),
91+
);
92+
log.lock()
93+
.expect("request log lock should not be poisoned")
94+
.push(format!("{} {}", req.method(), path));
95+
let response = queue
96+
.lock()
97+
.expect("response queue lock should not be poisoned")
98+
.pop_front()
99+
.expect("stub response should exist");
100+
Ok::<_, Infallible>(
101+
hyper::Response::builder()
102+
.status(response.status)
103+
.body(Full::new(Bytes::from(response.body)))
104+
.expect("stub response should build"),
105+
)
106+
}
107+
}),
108+
)
109+
.await;
110+
// The one-shot test client can close the Unix socket after the
111+
// response, which Hyper reports as a shutdown error. Let the
112+
// request log assertions below decide whether the stub served
113+
// the expected API calls.
114+
let _ = result;
115+
}
116+
let _ = std::fs::remove_file(&socket_path_for_task);
117+
});
118+
(socket_path, request_log, handle)
119+
}

0 commit comments

Comments
 (0)