mod common;
use common::{TestDir, TestGitRepo, run_pg_ephemeral};
#[tokio::test]
async fn test_populate_cache() {
let backend = ociman::test_backend_setup!();
let instance_name: pg_ephemeral::InstanceName = "populate-cache-test".parse().unwrap();
let name: ociman::reference::Name = format!("localhost/pg-ephemeral/{instance_name}")
.parse()
.unwrap();
for reference in backend.image_references_by_name(&name).await {
backend.remove_image_force(&reference).await;
}
let definition = pg_ephemeral::Definition::new(backend.clone(), pg_ephemeral::Image::default(), instance_name.clone())
.wait_available_timeout(std::time::Duration::from_secs(30))
.apply_script(
"schema-and-data".parse().unwrap(),
r##"psql -c "CREATE TABLE test_cache (id INTEGER PRIMARY KEY); INSERT INTO test_cache VALUES (42);""##,
pg_ephemeral::SeedCacheConfig::CommandHash,
)
.unwrap();
let loaded_seeds = definition.load_seeds(&instance_name).await.unwrap();
for seed in loaded_seeds.iter_seeds() {
assert!(!seed.cache_status().is_hit());
}
definition.populate_cache(&instance_name).await.unwrap();
let loaded_seeds = definition.load_seeds(&instance_name).await.unwrap();
for seed in loaded_seeds.iter_seeds() {
assert!(seed.cache_status().is_hit());
}
definition
.with_container(async |container| {
container
.with_connection(async |connection| {
let row: (i32,) = sqlx::query_as("SELECT id FROM test_cache")
.fetch_one(&mut *connection)
.await
.unwrap();
assert_eq!(row.0, 42);
})
.await;
})
.await
.unwrap();
for reference in backend.image_references_by_name(&name).await {
backend.remove_image_force(&reference).await;
}
}
#[tokio::test]
async fn test_populate_cache_runs_seeds_in_declaration_order() {
let backend = ociman::test_backend_setup!();
let instance_name: pg_ephemeral::InstanceName = "populate-cache-order-test".parse().unwrap();
let name: ociman::reference::Name = format!("localhost/pg-ephemeral/{instance_name}")
.parse()
.unwrap();
for reference in backend.image_references_by_name(&name).await {
backend.remove_image_force(&reference).await;
}
let definition = pg_ephemeral::Definition::new(
backend.clone(),
pg_ephemeral::Image::default(),
instance_name.clone(),
)
.wait_available_timeout(std::time::Duration::from_secs(30))
.apply_script(
"z-create-table".parse().unwrap(),
r#"psql -c "CREATE TABLE order_test (value INTEGER)""#,
pg_ephemeral::SeedCacheConfig::CommandHash,
)
.unwrap()
.apply_script(
"m-insert-row".parse().unwrap(),
r#"psql -c "INSERT INTO order_test VALUES (1)""#,
pg_ephemeral::SeedCacheConfig::CommandHash,
)
.unwrap()
.apply_script(
"a-update-row".parse().unwrap(),
r#"psql -c "UPDATE order_test SET value = 2 WHERE value = 1""#,
pg_ephemeral::SeedCacheConfig::CommandHash,
)
.unwrap();
definition.populate_cache(&instance_name).await.unwrap();
definition
.with_container(async |container| {
container
.with_connection(async |connection| {
let row: (i32,) = sqlx::query_as("SELECT value FROM order_test")
.fetch_one(&mut *connection)
.await
.unwrap();
assert_eq!(row.0, 2);
})
.await;
})
.await
.unwrap();
for reference in backend.image_references_by_name(&name).await {
backend.remove_image_force(&reference).await;
}
}
#[tokio::test]
async fn test_cache_status() {
let _backend = ociman::test_backend_setup!();
let repo = TestGitRepo::new("cache-test").await;
repo.write_file("schema.sql", "CREATE TABLE users (id INTEGER PRIMARY KEY);");
repo.write_file("data.sql", "INSERT INTO users (id) VALUES (1);");
let commit_hash = repo.commit("Initial").await;
let config_content = indoc::formatdoc! {r#"
image = "17.1"
[instances.main.seeds.a-schema]
type = "sql-file"
path = "schema.sql"
[instances.main.seeds.b-data-from-git]
type = "sql-file"
path = "data.sql"
git_revision = "{commit_hash}"
[instances.main.seeds.c-run-command]
type = "command"
command = "echo"
arguments = ["hello"]
cache.type = "command-hash"
[instances.main.seeds.d-run-script]
type = "script"
script = "echo 'hello world'"
"#};
repo.write_file("database.toml", &config_content);
let expected = indoc::indoc! {r#"
{
"instance": "main",
"image": "17.1",
"version": "0.3.0",
"seeds": [
{
"name": "a-schema",
"type": "sql-file",
"status": "miss",
"reference": "pg-ephemeral/main:16327c5c07464022d51b793714db22010fbe080a9b877ac00bd0d412b76ddc0f"
},
{
"name": "b-data-from-git",
"type": "sql-file-git-revision",
"status": "miss",
"reference": "pg-ephemeral/main:7079cc732d3efa227c859671f791e9f6aa9fb9091ea885a4f0e9f302fbabd0e5"
},
{
"name": "c-run-command",
"type": "command",
"status": "miss",
"reference": "pg-ephemeral/main:cf6167e847964b35835b974f12e55eebc0ebe7be221f90b19ad474445cba0fe6"
},
{
"name": "d-run-script",
"type": "script",
"status": "miss",
"reference": "pg-ephemeral/main:5134ca88c8e87bb5625785187b2414c6ef276439467f7972de5cfa1be9ebdcf4"
}
]
}
"#};
let stdout = run_pg_ephemeral(&["cache", "status", "--json"], &repo.path).await;
assert_eq!(stdout, expected);
}
#[tokio::test]
async fn test_cache_status_deterministic() {
let _backend = ociman::test_backend_setup!();
let dir = TestDir::new("cache-deterministic-test");
dir.write_file("schema.sql", "CREATE TABLE users (id INTEGER PRIMARY KEY);");
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.schema]
type = "sql-file"
path = "schema.sql"
"#},
);
let expected = indoc::indoc! {r#"
{
"instance": "main",
"image": "17.1",
"version": "0.3.0",
"seeds": [
{
"name": "schema",
"type": "sql-file",
"status": "miss",
"reference": "pg-ephemeral/main:16327c5c07464022d51b793714db22010fbe080a9b877ac00bd0d412b76ddc0f"
}
]
}
"#};
let stdout = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
assert_eq!(stdout, expected);
}
#[tokio::test]
async fn test_cache_status_change_with_content() {
let _backend = ociman::test_backend_setup!();
let dir = TestDir::new("cache-changes-test");
dir.write_file("schema.sql", "CREATE TABLE users (id INTEGER PRIMARY KEY);");
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.schema]
type = "sql-file"
path = "schema.sql"
"#},
);
let stdout1 = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
dir.write_file(
"schema.sql",
"CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);",
);
let stdout2 = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
assert_ne!(stdout2, stdout1);
}
#[tokio::test]
async fn test_cache_status_change_with_image() {
let _backend = ociman::test_backend_setup!();
let dir = TestDir::new("cache-image-test");
dir.write_file("schema.sql", "CREATE TABLE users (id INTEGER PRIMARY KEY);");
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.schema]
type = "sql-file"
path = "schema.sql"
"#},
);
let stdout1 = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.2"
[instances.main.seeds.schema]
type = "sql-file"
path = "schema.sql"
"#},
);
let stdout2 = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
assert_ne!(stdout2, stdout1);
}
#[tokio::test]
async fn test_cache_status_chain_propagates() {
let _backend = ociman::test_backend_setup!();
let dir = TestDir::new("cache-chain-test");
dir.write_file("first.sql", "CREATE TABLE first (id INTEGER);");
dir.write_file("second.sql", "CREATE TABLE second (id INTEGER);");
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.a-first]
type = "sql-file"
path = "first.sql"
[instances.main.seeds.b-second]
type = "sql-file"
path = "second.sql"
"#},
);
let expected_before = indoc::indoc! {r#"
{
"instance": "main",
"image": "17.1",
"version": "0.3.0",
"seeds": [
{
"name": "a-first",
"type": "sql-file",
"status": "miss",
"reference": "pg-ephemeral/main:e3a60e814a78d0b626ea6b6e5fb52f9e8dafc243044daf439dca77b902091ad5"
},
{
"name": "b-second",
"type": "sql-file",
"status": "miss",
"reference": "pg-ephemeral/main:f291554f3cf5a2c2b58cc835bca205dd15c0fb77e816850e306d4a2f2bb44b62"
}
]
}
"#};
let stdout1 = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
assert_eq!(stdout1, expected_before);
dir.write_file("first.sql", "CREATE TABLE first (id INTEGER, name TEXT);");
let stdout2 = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
assert_ne!(stdout2, expected_before);
}
#[tokio::test]
async fn test_cache_status_key_command() {
let _backend = ociman::test_backend_setup!();
let dir = TestDir::new("cache-key-command-test");
dir.write_file("version.txt", "1.0.0");
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.run-migrations]
type = "command"
command = "migrate"
arguments = ["up"]
[instances.main.seeds.run-migrations.cache]
type = "key-command"
command = "cat"
arguments = ["version.txt"]
"#},
);
let expected_before = indoc::indoc! {r#"
{
"instance": "main",
"image": "17.1",
"version": "0.3.0",
"seeds": [
{
"name": "run-migrations",
"type": "command",
"status": "miss",
"reference": "pg-ephemeral/main:98df5e005ab0053503fe193fcdd375993104f96e178f99863a30400b4218f098"
}
]
}
"#};
let stdout1 = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
assert_eq!(stdout1, expected_before);
dir.write_file("version.txt", "2.0.0");
let stdout2 = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
assert_ne!(stdout2, expected_before);
}
async fn seed_references(toml: &str) -> Vec<String> {
let instance_name = pg_ephemeral::InstanceName::MAIN;
let instances = pg_ephemeral::Config::load_toml(toml)
.unwrap()
.instance_map(&pg_ephemeral::config::InstanceDefinition::empty())
.unwrap();
let definition = instances
.get(&instance_name)
.unwrap()
.definition(&instance_name)
.await
.unwrap();
definition
.load_seeds(&instance_name)
.await
.unwrap()
.iter_seeds()
.map(|seed| seed.cache_status().reference().unwrap().to_string())
.collect()
}
#[tokio::test]
async fn test_cache_status_key_script_on_command_seed() {
let _backend = ociman::test_backend_setup!();
let baseline = seed_references(indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.run-migrations]
type = "command"
command = "migrate"
arguments = ["up"]
[instances.main.seeds.run-migrations.cache]
type = "key-script"
script = "echo version-1"
"#})
.await;
let after_key_change = seed_references(indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.run-migrations]
type = "command"
command = "migrate"
arguments = ["up"]
[instances.main.seeds.run-migrations.cache]
type = "key-script"
script = "echo version-2"
"#})
.await;
assert_ne!(after_key_change, baseline);
let after_args_change = seed_references(indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.run-migrations]
type = "command"
command = "migrate"
arguments = ["down"]
[instances.main.seeds.run-migrations.cache]
type = "key-script"
script = "echo version-1"
"#})
.await;
assert_ne!(after_args_change, baseline);
}
#[tokio::test]
async fn test_cli_key_script_failure_reports_display() {
let _backend = ociman::test_backend_setup!();
let dir = TestDir::new("cli-key-script-failure-display-test");
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.run-migrations]
type = "command"
command = "migrate"
arguments = ["up"]
[instances.main.seeds.run-migrations.cache]
type = "key-script"
script = "exit 1"
"#},
);
let pg_ephemeral_bin = env!("CARGO_BIN_EXE_pg-ephemeral");
let output = cmd_proc::Command::new(pg_ephemeral_bin)
.arguments(["cache", "status"])
.working_directory(&dir.path)
.stdout_capture()
.stderr_capture()
.accept_nonzero_exit()
.run()
.await
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert_eq!(
stderr,
indoc::indoc! {"
Error: Failed to load seed run-migrations: cache key script failed
caused by: command exited with exit status: 1
"},
);
}
#[tokio::test]
async fn test_cache_status_key_script_failure_propagates() {
let _backend = ociman::test_backend_setup!();
let instance_name = pg_ephemeral::InstanceName::MAIN;
let instances = pg_ephemeral::Config::load_toml(indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.run-migrations]
type = "command"
command = "migrate"
arguments = ["up"]
[instances.main.seeds.run-migrations.cache]
type = "key-script"
script = "exit 1"
"#})
.unwrap()
.instance_map(&pg_ephemeral::config::InstanceDefinition::empty())
.unwrap();
let definition = instances
.get(&instance_name)
.unwrap()
.definition(&instance_name)
.await
.unwrap();
let error = definition.load_seeds(&instance_name).await.unwrap_err();
assert!(
matches!(error, pg_ephemeral::LoadError::KeyScript { .. }),
"expected LoadError::KeyScript, got: {error:?}"
);
}
#[tokio::test]
async fn test_cache_status_key_script_on_script_seed() {
let _backend = ociman::test_backend_setup!();
let baseline = seed_references(indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.seed-data]
type = "script"
script = "psql -c 'SELECT 1'"
[instances.main.seeds.seed-data.cache]
type = "key-script"
script = "echo version-1"
"#})
.await;
let after_key_change = seed_references(indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.seed-data]
type = "script"
script = "psql -c 'SELECT 1'"
[instances.main.seeds.seed-data.cache]
type = "key-script"
script = "echo version-2"
"#})
.await;
assert_ne!(after_key_change, baseline);
let after_script_change = seed_references(indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.seed-data]
type = "script"
script = "psql -c 'SELECT 2'"
[instances.main.seeds.seed-data.cache]
type = "key-script"
script = "echo version-1"
"#})
.await;
assert_ne!(after_script_change, baseline);
}
#[tokio::test]
async fn test_cache_status_change_with_ssl() {
let _backend = ociman::test_backend_setup!();
let dir = TestDir::new("cache-ssl-test");
dir.write_file("schema.sql", "CREATE TABLE users (id INTEGER PRIMARY KEY);");
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.schema]
type = "sql-file"
path = "schema.sql"
"#},
);
let output_no_ssl = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[ssl_config]
hostname = "localhost"
[instances.main.seeds.schema]
type = "sql-file"
path = "schema.sql"
"#},
);
let output_with_ssl = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
assert_ne!(output_no_ssl, output_with_ssl);
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[ssl_config]
hostname = "example.com"
[instances.main.seeds.schema]
type = "sql-file"
path = "schema.sql"
"#},
);
let output_different_ssl = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
assert_ne!(output_with_ssl, output_different_ssl);
}
#[tokio::test]
async fn test_cache_status_container_script() {
let _backend = ociman::test_backend_setup!();
let dir = TestDir::new("cache-container-script-test");
dir.write_file(
"database.toml",
indoc::indoc! {r#"
image = "17.1"
[instances.main.seeds.install-ext]
type = "container-script"
script = "touch /container-script-marker"
"#},
);
let stdout = run_pg_ephemeral(&["cache", "status", "--json"], &dir.path).await;
let output: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(output["seeds"][0]["name"], "install-ext");
assert_eq!(output["seeds"][0]["type"], "container-script");
assert_eq!(output["seeds"][0]["status"], "miss");
assert!(output["seeds"][0]["reference"].is_string());
}
#[tokio::test]
async fn test_populate_cache_container_script() {
let backend = ociman::test_backend_setup!();
let instance_name: pg_ephemeral::InstanceName =
"populate-cache-container-script-test".parse().unwrap();
let name: ociman::reference::Name = format!("localhost/pg-ephemeral/{instance_name}")
.parse()
.unwrap();
for reference in backend.image_references_by_name(&name).await {
backend.remove_image_force(&reference).await;
}
let definition = pg_ephemeral::Definition::new(
backend.clone(),
pg_ephemeral::Image::default(),
instance_name.clone(),
)
.wait_available_timeout(std::time::Duration::from_secs(30))
.apply_container_script(
"create-marker".parse().unwrap(),
"touch /container-script-marker",
)
.unwrap();
let loaded_seeds = definition.load_seeds(&instance_name).await.unwrap();
for seed in loaded_seeds.iter_seeds() {
assert!(!seed.cache_status().is_hit());
}
definition.populate_cache(&instance_name).await.unwrap();
let loaded_seeds = definition.load_seeds(&instance_name).await.unwrap();
for seed in loaded_seeds.iter_seeds() {
assert!(seed.cache_status().is_hit());
}
definition
.with_container(async |container| {
container
.with_connection(async |connection| {
let row: (bool,) = sqlx::query_as("SELECT true")
.fetch_one(&mut *connection)
.await
.unwrap();
assert!(row.0);
})
.await;
})
.await
.unwrap();
for reference in backend.image_references_by_name(&name).await {
backend.remove_image_force(&reference).await;
}
}
#[tokio::test]
async fn test_container_script_with_pg_cron() {
let backend = ociman::test_backend_setup!();
let instance_name: pg_ephemeral::InstanceName =
"container-script-pg-cron-test".parse().unwrap();
let name: ociman::reference::Name = format!("localhost/pg-ephemeral/{instance_name}")
.parse()
.unwrap();
for reference in backend.image_references_by_name(&name).await {
backend.remove_image_force(&reference).await;
}
let definition = pg_ephemeral::Definition::new(
backend.clone(),
"17".parse().unwrap(),
instance_name.clone(),
)
.wait_available_timeout(std::time::Duration::from_secs(30))
.apply_container_script(
"install-pg-cron".parse().unwrap(),
"apt-get update && apt-get install -y --no-install-recommends postgresql-17-cron \
&& printf '#!/bin/bash\\necho \"shared_preload_libraries = '\"'\"'pg_cron'\"'\"'\" >> \"$PGDATA/postgresql.conf\"\\n' \
> /docker-entrypoint-initdb.d/pg-cron.sh \
&& chmod +x /docker-entrypoint-initdb.d/pg-cron.sh",
)
.unwrap()
.apply_script(
"enable-pg-cron".parse().unwrap(),
r#"psql -c "CREATE EXTENSION pg_cron""#,
pg_ephemeral::SeedCacheConfig::CommandHash,
)
.unwrap();
definition
.with_container(async |container| {
container
.with_connection(async |connection| {
let row: (String,) = sqlx::query_as(
"SELECT extname::text FROM pg_extension WHERE extname = 'pg_cron'",
)
.fetch_one(&mut *connection)
.await
.unwrap();
assert_eq!(row.0, "pg_cron");
})
.await;
})
.await
.unwrap();
for reference in backend.image_references_by_name(&name).await {
backend.remove_image_force(&reference).await;
}
}
#[allow(clippy::async_yields_async)]
#[tokio::test]
async fn test_stale_connection_terminated_before_stop() {
let backend = ociman::test_backend_setup!();
let definition = common::test_definition(backend);
let sleep_handle = definition
.with_container(async |container| {
let config = container.client_config().to_sqlx_connect_options().unwrap();
let mut connection = sqlx::ConnectOptions::connect(&config).await.unwrap();
tokio::spawn(async move {
sqlx::query("SELECT pg_sleep(3600)")
.execute(&mut connection)
.await
})
})
.await
.unwrap();
let error = sleep_handle.await.unwrap().unwrap_err();
match error {
sqlx::Error::Database(ref db_error) => {
assert_eq!(db_error.code().as_deref(), Some("57P01"));
}
_ => panic!("Expected database error 57P01 (admin_shutdown), got: {error}"),
}
}