Skip to content

Commit 41690ea

Browse files
committed
refactor: [#116] improve container test utilities structure
- Implement ContainerId newtype with validation for type safety - Split container utilities into independent modules: - container_id.rs: validated container ID type - running_binary_container.rs: running container with binary - ubuntu_container_builder.rs: builder pattern for container creation - Remove helpers.rs by moving functions to type methods - Remove ubuntu.rs re-export module for direct imports - Rename types for semantic clarity: - UbuntuTestContainer → RunningBinaryContainer - UbuntuContainerWithBinary → UbuntuContainerBuilder - Add 'hexdigit' to project dictionary for spell checking This refactoring improves modularity, type safety, and maintainability while preserving all existing functionality. All tests remain passing.
1 parent a3b70c4 commit 41690ea

File tree

8 files changed

+262
-240
lines changed

8 files changed

+262
-240
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
//! Docker container identifier type
2+
3+
use std::ffi::OsStr;
4+
use std::fmt;
5+
6+
/// Docker container identifier
7+
///
8+
/// A validated Docker container ID, which is a hexadecimal string.
9+
/// Docker generates these IDs and guarantees they contain only hex characters (0-9, a-f).
10+
///
11+
/// # Examples
12+
///
13+
/// ```
14+
/// # use std::path::PathBuf;
15+
/// // Container IDs come from Docker/testcontainers and are always valid hex strings
16+
/// let id = ContainerId::new("a1b2c3d4e5f6".to_string()).expect("valid hex string");
17+
/// ```
18+
#[derive(Debug, Clone, PartialEq, Eq)]
19+
pub struct ContainerId(String);
20+
21+
impl ContainerId {
22+
/// Create a new container ID with validation
23+
///
24+
/// # Arguments
25+
///
26+
/// * `id` - The container ID string (must be hexadecimal)
27+
///
28+
/// # Returns
29+
///
30+
/// `Ok(ContainerId)` if valid, `Err` with error message if invalid
31+
///
32+
/// # Validation Rules
33+
///
34+
/// - Must not be empty
35+
/// - Must contain only hexadecimal characters (0-9, a-f, A-F)
36+
/// - Typically 12 characters (short form) or 64 characters (full SHA256)
37+
pub fn new(id: String) -> Result<Self, String> {
38+
if id.is_empty() {
39+
return Err("Container ID cannot be empty".to_string());
40+
}
41+
42+
if !id.chars().all(|c| c.is_ascii_hexdigit()) {
43+
return Err(format!(
44+
"Container ID must contain only hexadecimal characters, got: '{id}'"
45+
));
46+
}
47+
48+
Ok(Self(id))
49+
}
50+
}
51+
52+
impl fmt::Display for ContainerId {
53+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54+
write!(f, "{}", self.0)
55+
}
56+
}
57+
58+
impl AsRef<OsStr> for ContainerId {
59+
fn as_ref(&self) -> &OsStr {
60+
OsStr::new(&self.0)
61+
}
62+
}

packages/dependency-installer/tests/containers/helpers.rs

Lines changed: 0 additions & 94 deletions
This file was deleted.

packages/dependency-installer/tests/containers/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
//!
33
//! This module provides helper types and functions for managing test containers.
44
5-
pub mod helpers;
6-
pub mod ubuntu;
5+
pub(super) mod container_id;
6+
pub(super) mod running_binary_container;
7+
pub mod ubuntu_container_builder;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//! Running container with an installed binary
2+
3+
use std::path::Path;
4+
use std::process::Command;
5+
6+
use testcontainers::{ContainerAsync, GenericImage};
7+
8+
use super::container_id::ContainerId;
9+
10+
/// A running Ubuntu container with a binary installed and ready to execute
11+
///
12+
/// This struct provides methods for executing commands and managing a running
13+
/// Ubuntu container that has been prepared with a binary. It handles the container
14+
/// lifecycle, ensuring the container stays alive while tests run, and provides
15+
/// convenient methods for command execution and file operations.
16+
pub struct RunningBinaryContainer {
17+
// Keep a reference to the container so it stays alive
18+
#[allow(dead_code)]
19+
container: ContainerAsync<GenericImage>,
20+
container_id: ContainerId,
21+
}
22+
23+
impl RunningBinaryContainer {
24+
/// Create a new running binary container
25+
///
26+
/// # Arguments
27+
///
28+
/// * `container` - The running Docker container
29+
/// * `container_id` - The validated container ID
30+
pub(super) fn new(container: ContainerAsync<GenericImage>, container_id: ContainerId) -> Self {
31+
Self {
32+
container,
33+
container_id,
34+
}
35+
}
36+
37+
/// Execute a command in the container and return stdout
38+
///
39+
/// # Arguments
40+
///
41+
/// * `command` - Command and arguments to execute
42+
///
43+
/// # Returns
44+
///
45+
/// The combined stdout and stderr output as a string
46+
///
47+
/// # Note
48+
///
49+
/// The output combines stderr and stdout because the CLI uses tracing which writes
50+
/// logs to stderr, while user-facing messages go to stdout. We need both for
51+
/// comprehensive test assertions. Stderr is placed first to maintain chronological
52+
/// order of log messages relative to output.
53+
pub fn exec(&self, command: &[&str]) -> String {
54+
let output = Command::new("docker")
55+
.arg("exec")
56+
.arg(&self.container_id)
57+
.args(command)
58+
.output()
59+
.expect("Failed to execute docker exec command");
60+
61+
// Combine stderr (logs) and stdout (user messages) to capture all output
62+
let stdout = String::from_utf8_lossy(&output.stdout);
63+
let stderr = String::from_utf8_lossy(&output.stderr);
64+
format!("{stderr}{stdout}")
65+
}
66+
67+
/// Execute a command and return the exit code
68+
///
69+
/// # Arguments
70+
///
71+
/// * `command` - Command and arguments to execute
72+
///
73+
/// # Returns
74+
///
75+
/// The exit code of the command, or 1 if the process was terminated by a signal
76+
///
77+
/// # Note
78+
///
79+
/// If the process was terminated by a signal (returns None from `code()`), we return 1
80+
/// to indicate failure rather than 0, which would incorrectly suggest success.
81+
pub fn exec_with_exit_code(&self, command: &[&str]) -> i32 {
82+
let status = Command::new("docker")
83+
.arg("exec")
84+
.arg(&self.container_id)
85+
.args(command)
86+
.status()
87+
.expect("Failed to execute docker exec command");
88+
89+
// Return 1 (failure) if terminated by signal, otherwise use actual exit code
90+
status.code().unwrap_or(1)
91+
}
92+
93+
/// Copy a file from the host into this running container
94+
///
95+
/// This method uses Docker CLI to copy files into the running container.
96+
///
97+
/// # Arguments
98+
///
99+
/// * `source_path` - Path to the file on the host system
100+
/// * `dest_path` - Destination path inside the container
101+
///
102+
/// # Panics
103+
///
104+
/// Panics if the Docker copy command fails
105+
pub(super) fn copy_file_to_container(&self, source_path: &Path, dest_path: &str) {
106+
let output = Command::new("docker")
107+
.arg("cp")
108+
.arg(source_path)
109+
.arg(format!("{}:{dest_path}", self.container_id))
110+
.output()
111+
.expect("Failed to execute docker cp command");
112+
113+
if !output.status.success() {
114+
let stderr = String::from_utf8_lossy(&output.stderr);
115+
panic!("Failed to copy file to container: {stderr}");
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)