use std::sync::{Arc, Mutex};
use color_eyre::eyre::{Result, ensure, eyre};
use rstest::rstest;
use serial_test::serial;
use super::*;
use crate::cluster::extension_hook::populate_cache_on_miss;
#[path = "startup_extension_fixtures.rs"]
mod fixtures;
#[cfg(unix)]
use fixtures::manifest_observing_the_lock;
use fixtures::{
PROBE_FILES,
failing_bootstrap,
lifecycle_case,
lock_is_free,
ordering_bootstrap,
seed_cache_hit,
};
fn assert_installed_between_setup_and_start(
operations: &Mutex<Vec<String>>,
bootstrap: &TestBootstrapSettings,
) -> Result<()> {
let recorded = operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
ensure!(
recorded == ["setup", "start(extension_present=true)"],
"expected install between Setup and Start, recorded {recorded:?}"
);
ensure!(
bootstrap.installed_extensions.len() == 1,
"the handle must report the installed extension"
);
Ok(())
}
fn assert_stopped_before_start(
operations: &Mutex<Vec<String>>,
err: &crate::error::BootstrapError,
) -> Result<()> {
ensure!(
err.kind() == crate::error::BootstrapErrorKind::ExtensionManifestInvalid,
"expected ExtensionManifestInvalid, got {:?}",
err.kind()
);
let recorded = operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
ensure!(
recorded == ["setup"],
"Start must not be dispatched after a hook failure, recorded {recorded:?}"
);
Ok(())
}
#[rstest]
#[serial(worker_hook)]
fn root_lifecycle_stops_when_the_extension_hook_fails(
#[from(root_setup_paths)] root_setup_paths_res: Result<Arc<RootSetupPaths>>,
) -> Result<()> {
let paths = root_setup_paths_res?;
let case = lifecycle_case(&paths, failing_bootstrap)?;
let err = start_postgres(
&case.runtime,
case.bootstrap,
&case.env_vars,
&case.cache_config,
)
.err()
.ok_or_else(|| eyre!("a broken manifest must stop the lifecycle"))?;
assert_stopped_before_start(&case.hook.operations, &err)
}
#[rstest]
#[serial(worker_hook)]
fn a_cache_hit_with_extensions_retakes_the_setup_lock_for_the_hook(
#[from(root_setup_paths)] root_setup_paths_res: Result<Arc<RootSetupPaths>>,
) -> Result<()> {
let paths = root_setup_paths_res?;
seed_cache_hit(&paths.cache_dir)?;
let case = lifecycle_case(&paths, ordering_bootstrap)?;
let outcome = start_postgres(
&case.runtime,
case.bootstrap,
&case.env_vars,
&case.cache_config,
)?;
assert_installed_between_setup_and_start(&case.hook.operations, &outcome.bootstrap)?;
ensure!(
lock_is_free(&paths.install_dir),
"the setup lock is free once the start returns"
);
Ok(())
}
#[cfg(unix)]
#[rstest]
#[serial(worker_hook)]
fn the_hook_holds_the_setup_lock_while_it_installs_after_a_cache_hit(
#[from(root_setup_paths)] root_setup_paths_res: Result<Arc<RootSetupPaths>>,
) -> Result<()> {
let paths = root_setup_paths_res?;
seed_cache_hit(&paths.cache_dir)?;
let case = lifecycle_case(&paths, ordering_bootstrap)?;
let observer = manifest_observing_the_lock(&case.bootstrap, &paths.install_dir)?;
start_postgres(
&case.runtime,
case.bootstrap,
&case.env_vars,
&case.cache_config,
)?;
let held = observer
.join()
.map_err(|_| eyre!("the observer thread panicked"))??;
ensure!(held, "the hook must hold the setup lock while it installs");
ensure!(
lock_is_free(&paths.install_dir),
"the setup lock is free once the start returns"
);
Ok(())
}
#[cfg(all(unix, feature = "async-api"))]
#[rstest]
#[serial(worker_hook)]
fn the_async_hook_holds_the_setup_lock_while_it_installs_after_a_cache_hit(
#[from(root_setup_paths)] root_setup_paths_res: Result<Arc<RootSetupPaths>>,
) -> Result<()> {
let paths = root_setup_paths_res?;
seed_cache_hit(&paths.cache_dir)?;
let case = lifecycle_case(&paths, ordering_bootstrap)?;
let observer = manifest_observing_the_lock(&case.bootstrap, &paths.install_dir)?;
case.runtime.block_on(start_postgres_async(
case.bootstrap,
&case.env_vars,
&case.cache_config,
))?;
let held = observer
.join()
.map_err(|_| eyre!("the observer thread panicked"))??;
ensure!(held, "the hook must hold the setup lock while it installs");
ensure!(
lock_is_free(&paths.install_dir),
"the setup lock is free once the start returns"
);
Ok(())
}
#[cfg(feature = "async-api")]
#[rstest]
#[serial(worker_hook)]
fn async_root_lifecycle_stops_when_the_extension_hook_fails(
#[from(root_setup_paths)] root_setup_paths_res: Result<Arc<RootSetupPaths>>,
) -> Result<()> {
let paths = root_setup_paths_res?;
let case = lifecycle_case(&paths, failing_bootstrap)?;
let err = case
.runtime
.block_on(start_postgres_async(
case.bootstrap,
&case.env_vars,
&case.cache_config,
))
.err()
.ok_or_else(|| eyre!("a broken manifest must stop the lifecycle"))?;
assert_stopped_before_start(&case.hook.operations, &err)
}
#[rstest]
#[serial(worker_hook)]
fn setup_only_lifecycle_stops_when_the_extension_hook_fails(
#[from(root_setup_paths)] root_setup_paths_res: Result<Arc<RootSetupPaths>>,
) -> Result<()> {
let paths = root_setup_paths_res?;
let case = lifecycle_case(&paths, failing_bootstrap)?;
let err = setup_lifecycle(
&case.runtime,
case.bootstrap,
&case.env_vars,
&case.cache_config,
)
.err()
.ok_or_else(|| eyre!("a broken manifest must stop the setup-only lifecycle"))?;
assert_stopped_before_start(&case.hook.operations, &err)
}
#[rstest]
#[serial(worker_hook)]
fn root_lifecycle_installs_extensions_between_setup_and_start(
#[from(root_setup_paths)] root_setup_paths_res: Result<Arc<RootSetupPaths>>,
) -> Result<()> {
let paths = root_setup_paths_res?;
let case = lifecycle_case(&paths, ordering_bootstrap)?;
let outcome = start_postgres(
&case.runtime,
case.bootstrap,
&case.env_vars,
&case.cache_config,
)?;
assert_installed_between_setup_and_start(&case.hook.operations, &outcome.bootstrap)
}
#[cfg(feature = "async-api")]
#[rstest]
#[serial(worker_hook)]
fn async_root_lifecycle_installs_extensions_between_setup_and_start(
#[from(root_setup_paths)] root_setup_paths_res: Result<Arc<RootSetupPaths>>,
) -> Result<()> {
let paths = root_setup_paths_res?;
let case = lifecycle_case(&paths, ordering_bootstrap)?;
let outcome = case.runtime.block_on(start_postgres_async(
case.bootstrap,
&case.env_vars,
&case.cache_config,
))?;
assert_installed_between_setup_and_start(&case.hook.operations, &outcome.bootstrap)
}
#[rstest]
#[serial(worker_hook)]
fn setup_only_lifecycle_installs_extensions_after_setup(
#[from(root_setup_paths)] root_setup_paths_res: Result<Arc<RootSetupPaths>>,
) -> Result<()> {
let paths = root_setup_paths_res?;
let case = lifecycle_case(&paths, ordering_bootstrap)?;
let prepared = setup_lifecycle(
&case.runtime,
case.bootstrap,
&case.env_vars,
&case.cache_config,
)?;
let recorded = case
.hook
.operations
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
ensure!(
recorded == ["setup"],
"setup-only must not start: {recorded:?}"
);
let versioned = paths.install_dir.join(TEST_POSTGRES_VERSION);
ensure!(
PROBE_FILES
.iter()
.all(|(name, _)| versioned.join(name).is_file()),
"extension files must be installed by the setup-only path"
);
ensure!(
prepared.installed_extensions.len() == 1,
"report carried through"
);
Ok(())
}
#[rstest]
fn populate_cache_on_miss_only_populates_on_cache_miss(
#[from(cache_population_paths)] cache_population_paths_res: Result<CachePopulationPaths>,
) -> Result<()> {
let cache_population_paths = cache_population_paths_res?;
let mut bootstrap = dummy_settings(ExecutionPrivileges::Unprivileged);
bootstrap.settings.installation_dir = cache_population_paths
.install_dir
.clone()
.into_std_path_buf();
bootstrap.settings.data_dir = cache_population_paths.data_dir.into_std_path_buf();
let cache_config = BinaryCacheConfig::with_dir(cache_population_paths.cache_dir.clone());
let hit = PostSetup {
cache_config: &cache_config,
cache_hit: true,
install_lock: InstallLock::Held,
};
populate_cache_on_miss(hit, &bootstrap);
ensure!(
!cache_population_paths.marker_path.exists(),
"cache marker should remain absent on cache hit"
);
let miss = PostSetup {
cache_config: &cache_config,
cache_hit: false,
install_lock: InstallLock::Held,
};
populate_cache_on_miss(miss, &bootstrap);
ensure!(
cache_population_paths.marker_path.exists(),
"cache marker should be written on cache miss"
);
Ok(())
}