use std::{
path::{Path, PathBuf},
process::Command,
};
use tempfile::TempDir;
use super::{
BALLAST, Backend, DropArgs, FROZEN, HoldAction, MARKER, ReattachArgs, assert_marker,
assert_state, backup, clear_records, hold, lifecycle, restore_err, sole_hold, write,
};
use crate::actions::canopy::backup::{
hold::{CaptureState, HeldCapture, HoldRecord},
postgresql::vss,
};
const BOGUS_DEVICE: &str = r"\\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy999\";
struct Harness {
backup_type: String,
backups_dir: TempDir,
data_dir: PathBuf,
port: u16,
}
impl Harness {
async fn new(backup_type: &str) -> Self {
clear_records(backup_type).await;
let data_dir = data_dir();
assert!(
data_dir.join("PG_VERSION").is_file(),
"{} is not a postgres data directory; the VSS lifecycle needs the host's own \
cluster (set PGDATA)",
data_dir.display(),
);
let backups_dir = TempDir::new().expect("a directory for the backup def");
std::fs::write(
backups_dir.path().join("def.toml"),
format!(
"type = \"{backup_type}\"\n\
[postgresql]\n\
cluster = \"data\"\n\
data_dir = '{}'\n",
data_dir.display(),
),
)
.expect("writing the backup def");
Self {
backup_type: backup_type.to_owned(),
backups_dir,
data_dir,
port: port(),
}
}
}
impl Backend for Harness {
fn backup_type(&self) -> &str {
&self.backup_type
}
fn backups_dir(&self) -> &Path {
self.backups_dir.path()
}
fn data_dir(&self) -> &Path {
&self.data_dir
}
fn expected_backend(&self) -> &'static str {
"vss"
}
async fn cluster_is_up(&self) -> bool {
let bin = crate::find_postgres::find_postgres_bin("pg_isready")
.expect("pg_isready ships with the postgres install");
Command::new(bin)
.args(["-h", "127.0.0.1", "-p", &self.port.to_string()])
.status()
.map(|status| status.success())
.unwrap_or(false)
}
async fn capture_present(&self, record: &HoldRecord) -> bool {
let HeldCapture::Vss { shadow_id, .. } = &record.capture else {
panic!("the vss backend held a {} capture", record.capture.backend());
};
vss::held_present(shadow_id).await
}
}
impl Drop for Harness {
fn drop(&mut self) {
let _ = std::fs::remove_file(self.data_dir.join(MARKER));
let _ = std::fs::remove_file(self.data_dir.join(BALLAST));
}
}
#[tokio::test]
#[ignore = "needs Windows admin, VSS and postgres; run in the `vss / wmi e2e` CI job"]
async fn a_vss_hold_is_taken_used_and_released() {
lifecycle(&Harness::new("hold-e2e-vss").await).await;
}
#[tokio::test]
#[ignore = "needs Windows admin, VSS and postgres; run in the `vss / wmi e2e` CI job"]
async fn a_hold_whose_junction_went_stale_is_reattached() {
let backend = Harness::new("hold-e2e-vss-reboot").await;
let data_dir = backend.data_dir().to_path_buf();
write(&data_dir.join(MARKER), FROZEN);
backup(backend.backup_type(), backend.backups_dir())
.await
.expect("taking a capture-only hold");
let held = sole_hold(backend.backup_type()).await;
assert_eq!(held.capture.backend(), "vss");
assert_state(&held, CaptureState::Present, "just after it was taken").await;
let HeldCapture::Vss { junction, .. } = &held.capture else {
panic!("the vss backend held a {} capture", held.capture.backend());
};
point_at_a_bogus_device(junction);
assert_state(
&held,
CaptureState::Detached,
"with its junction pointing at a device VSS has not handed out",
)
.await;
let err = restore_err(&held, backend.backups_dir()).await;
assert!(
err.contains("not mounted") && err.contains("reattach"),
"a restore from a detached hold has to name the state and the remedy: {err}",
);
hold(HoldAction::Reattach(ReattachArgs { id: held.id.clone() }))
.await
.expect("reattaching a hold whose junction went stale");
assert_state(&held, CaptureState::Present, "after reattaching it").await;
assert_marker(&held.source, FROZEN, "the reattached capture");
hold(HoldAction::Drop(DropArgs { id: held.id.clone() }))
.await
.expect("dropping the reattached hold");
assert!(
!backend.capture_present(&held).await,
"the shadow copy behind hold {} outlived the drop",
held.id,
);
}
fn point_at_a_bogus_device(junction: &Path) {
let _ = std::fs::remove_dir(junction);
junction::create(BOGUS_DEVICE, junction)
.expect("repointing the junction at a device number VSS has not handed out");
}
fn installed_major() -> String {
let base = std::env::var_os("ProgramFiles")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(r"C:\Program Files"))
.join("PostgreSQL");
std::fs::read_dir(&base)
.unwrap_or_else(|err| panic!("reading {}: {err}", base.display()))
.flatten()
.filter_map(|entry| entry.file_name().into_string().ok())
.filter_map(|name| name.parse::<u32>().ok())
.max()
.unwrap_or_else(|| panic!("no versioned postgres install under {}", base.display()))
.to_string()
}
fn data_dir() -> PathBuf {
if let Some(pgdata) = std::env::var_os("PGDATA") {
return PathBuf::from(pgdata);
}
std::env::var_os("ProgramFiles")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(r"C:\Program Files"))
.join("PostgreSQL")
.join(installed_major())
.join("data")
}
fn port() -> u16 {
std::env::var("PGPORT")
.ok()
.and_then(|raw| raw.trim().parse().ok())
.unwrap_or(5432)
}