Skip to main content

aster_forge_test/
mysql.rs

1//! Shared reusable MySQL container for integration tests.
2//!
3//! The container provides a root connection to the default `mysql` system database. Creating
4//! per-test databases and granting product users stays with the product test harness, which can
5//! register database names via [`crate::state::SharedContainerState::remember_resource`] so stale
6//! databases are pruned on later runs.
7
8use crate::state::{ContainerLease, ContainerStateLock};
9use crate::suite::TestContainerSuite;
10use testcontainers::core::{ContainerAsync, IntoContainerPort, WaitFor};
11use testcontainers::{GenericImage, ImageExt, ReuseDirective, runners::AsyncRunner};
12
13/// Handle to the suite's shared MySQL container.
14pub struct MysqlTestContainer {
15    root_url: String,
16    suite: TestContainerSuite,
17    stale_resources: Vec<String>,
18    _container: ContainerAsync<GenericImage>,
19    _lease: ContainerLease,
20}
21
22impl MysqlTestContainer {
23    /// Starts (or reuses) the shared MySQL container with `root`/`rootpass` credentials.
24    pub async fn start(suite: &TestContainerSuite) -> Self {
25        let lock = ContainerStateLock::acquire(suite, "mysql");
26        let mut state = lock.load();
27        let stale_resources = state.prune_stale();
28        state.register_pid(std::process::id());
29        lock.save(&state);
30
31        let container = GenericImage::new("mysql", "8.4")
32            .with_exposed_port(IntoContainerPort::tcp(3306))
33            .with_wait_for(WaitFor::message_on_stdout("ready for connections"))
34            .with_container_name(suite.container_name("mysql"))
35            .with_reuse(ReuseDirective::Always)
36            .with_env_var("MYSQL_ROOT_PASSWORD", "rootpass")
37            .start()
38            .await
39            .expect("failed to start MySQL test container");
40        let port = container
41            .get_host_port_ipv4(IntoContainerPort::tcp(3306))
42            .await
43            .expect("MySQL test port should be exposed");
44        drop(lock);
45
46        Self {
47            root_url: format!("mysql://root:rootpass@127.0.0.1:{port}/mysql"),
48            suite: suite.clone(),
49            stale_resources,
50            _container: container,
51            _lease: ContainerLease::new(suite.clone(), "mysql"),
52        }
53    }
54
55    /// Returns the root URL pointing at the `mysql` system database.
56    pub fn root_url(&self) -> &str {
57        &self.root_url
58    }
59
60    /// Builds a URL for a database created inside this container.
61    pub fn database_url(&self, database: &str) -> String {
62        self.root_url
63            .rsplit_once('/')
64            .map(|(base, _)| format!("{base}/{database}"))
65            .unwrap_or_else(|| self.root_url.clone())
66    }
67
68    /// Returns resources left by test processes that no longer exist.
69    pub fn stale_resources(&self) -> &[String] {
70        &self.stale_resources
71    }
72
73    /// Registers a product-owned resource, such as a per-test database name.
74    pub fn remember_resource(&self, resource: &str) {
75        let lock = ContainerStateLock::acquire(&self.suite, "mysql");
76        let mut state = lock.load();
77        state.remember_resource(std::process::id(), resource);
78        lock.save(&state);
79    }
80
81    /// Removes a resource after the product test harness cleaned it up.
82    pub fn forget_resource(&self, resource: &str) {
83        let lock = ContainerStateLock::acquire(&self.suite, "mysql");
84        let mut state = lock.load();
85        state.forget_resource(std::process::id(), resource);
86        lock.save(&state);
87    }
88}