use std::path::Path;
use std::path::PathBuf;
use std::str::FromStr;
use aws_sdk_s3::primitives::ByteStream;
use test_log::test;
use crate::Res;
use crate::checksum::calculate_hash;
use crate::flow;
use crate::io::remote::HostConfig;
use crate::io::remote::Remote;
use crate::io::remote::mocks::MockRemote;
use crate::io::storage::LocalStorage;
use crate::io::storage::Storage;
use crate::lineage::DomainLineage;
use crate::lineage::InstalledPackageStatus;
use crate::lineage::PackageLineage;
use crate::lineage::UpstreamState;
use crate::manifest::Manifest;
use crate::manifest::ManifestRow;
use crate::paths;
use crate::paths::DomainPaths;
use quilt_uri::ManifestUri;
use quilt_uri::Namespace;
use quilt_uri::S3Uri;
const BUCKET: &str = "b";
async fn publish_row(
remote: &MockRemote,
scratch: &Path,
host_config: &HostConfig,
logical_key: &str,
version: Option<&str>,
body: &[u8],
) -> Res<ManifestRow> {
let storage = LocalStorage::new();
let local = scratch.join(format!("{logical_key}.{}", uuid::Uuid::new_v4()));
storage
.write_byte_stream(&local, ByteStream::from(body.to_vec()))
.await?;
let row = calculate_hash(&storage, &local, Path::new(logical_key), host_config).await?;
let physical_key = match version {
Some(version) => format!("s3://{BUCKET}/f/a/{logical_key}?versionId={version}"),
None => format!("s3://{BUCKET}/f/a/{logical_key}"),
};
remote
.put_object(None, &S3Uri::from_str(&physical_key)?, body.to_vec())
.await?;
Ok(ManifestRow {
physical_key,
..row
})
}
async fn publish_manifest(remote: &MockRemote, hash: &str, rows: Vec<ManifestRow>) -> Res {
let manifest = Manifest {
rows,
..Manifest::default()
};
remote
.put_object(
None,
&S3Uri::from_str(&format!("s3://{BUCKET}/.quilt/packages/{hash}"))?,
ByteStream::from(&manifest),
)
.await
}
struct Rev1Installed {
storage: LocalStorage,
remote: MockRemote,
domain_paths: DomainPaths,
namespace: Namespace,
package_home: PathBuf,
lineage: PackageLineage,
_dirs: [tempfile::TempDir; 3],
}
impl Rev1Installed {
async fn new(versioned: bool) -> Res<Self> {
let v = |version| versioned.then_some(version);
let host_config = HostConfig::default();
let scratch = tempfile::TempDir::new()?;
let remote = MockRemote::default();
let namespace: Namespace = ("f", "a").into();
let same = publish_row(
&remote,
scratch.path(),
&host_config,
"same.txt",
v("v1"),
b"same",
)
.await?;
let old = publish_row(
&remote,
scratch.path(),
&host_config,
"changes.txt",
v("v1"),
b"one",
)
.await?;
let new = publish_row(
&remote,
scratch.path(),
&host_config,
"changes.txt",
v("v2"),
b"two",
)
.await?;
publish_manifest(&remote, "REV1", vec![old, same.clone()]).await?;
publish_manifest(&remote, "REV2", vec![new, same]).await?;
remote
.put_object(
None,
&S3Uri::from_str(&format!("s3://{BUCKET}/.quilt/named_packages/f/a/latest"))?,
b"REV2".to_vec(),
)
.await?;
let storage = LocalStorage::new();
let (domain_lineage, home_dir) = DomainLineage::from_temp_dir()?;
let home = domain_lineage.home.clone();
let (domain_paths, paths_dir) = DomainPaths::from_temp_dir()?;
let rev1 = ManifestUri {
bucket: BUCKET.to_string(),
namespace: namespace.clone(),
hash: "REV1".to_string(),
origin: None,
};
let domain_lineage =
flow::install_package(domain_lineage, &domain_paths, &storage, &remote, &rev1).await?;
let lineage = domain_lineage.packages[&namespace].clone();
assert_eq!(lineage.latest_hash, "REV2");
Ok(Self {
package_home: paths::package_home(&home, &namespace),
storage,
remote,
domain_paths,
namespace,
lineage,
_dirs: [scratch, home_dir, paths_dir],
})
}
fn installed_manifest(&self) -> PathBuf {
self.domain_paths
.installed_manifest(&self.namespace, "REV1")
}
async fn download_all(&self) -> Res<(PackageLineage, Vec<PathBuf>)> {
let all = [PathBuf::from("changes.txt"), PathBuf::from("same.txt")];
self.download(self.lineage.clone(), &all).await
}
async fn download(
&self,
lineage: PackageLineage,
paths: &[PathBuf],
) -> Res<(PackageLineage, Vec<PathBuf>)> {
let mut manifest = Manifest::from_path(&self.storage, &self.installed_manifest()).await?;
flow::install_paths(
lineage,
&mut manifest,
&self.domain_paths,
self.package_home.clone(),
self.namespace.clone(),
&self.storage,
&self.remote,
&paths.iter().collect::<Vec<_>>(),
)
.await
}
async fn status(&self, lineage: PackageLineage) -> Res<InstalledPackageStatus> {
let manifest = Manifest::from_path(&self.storage, &self.installed_manifest()).await?;
let (_, status) = flow::status(
lineage,
&self.storage,
&manifest,
&self.package_home,
HostConfig::default(),
)
.await?;
Ok(status)
}
}
#[test(tokio::test)]
async fn downloading_an_older_revision_leaves_untouched_files_unchanged() -> Res {
let package = Rev1Installed::new(true).await?;
let (lineage, skipped) = package.download_all().await?;
assert!(skipped.is_empty(), "skipped {skipped:?}");
let on_disk = tokio::fs::read(package.package_home.join("changes.txt")).await?;
assert_eq!(on_disk, b"one");
let status = package.status(lineage).await?;
assert_eq!(status.upstream_state, UpstreamState::Behind);
assert!(
status.changes.is_empty(),
"untouched files read as changed: {:?}",
status.changes.keys().collect::<Vec<_>>()
);
Ok(())
}
#[test(tokio::test)]
async fn an_unversioned_key_skips_the_replaced_row_and_installs_the_rest() -> Res {
let package = Rev1Installed::new(false).await?;
let (lineage, skipped) = package.download_all().await?;
assert_eq!(skipped, vec![PathBuf::from("changes.txt")]);
assert_eq!(
tokio::fs::read(package.package_home.join("same.txt")).await?,
b"same"
);
assert!(
!package
.storage
.exists(package.package_home.join("changes.txt"))
.await,
"revision 2's bytes were placed under revision 1's row"
);
assert_eq!(
lineage.paths.keys().collect::<Vec<_>>(),
vec![&PathBuf::from("same.txt")]
);
let status = package.status(lineage).await?;
assert!(
status.changes.is_empty(),
"untouched files read as changed: {:?}",
status.changes.keys().collect::<Vec<_>>()
);
Ok(())
}
#[test(tokio::test)]
async fn retrying_a_skipped_path_does_not_take_wrong_bytes_from_the_store() -> Res {
let package = Rev1Installed::new(false).await?;
let (lineage, skipped) = package.download_all().await?;
let (lineage, skipped) = package.download(lineage, &skipped).await?;
assert_eq!(skipped, vec![PathBuf::from("changes.txt")]);
assert!(
!package
.storage
.exists(package.package_home.join("changes.txt"))
.await,
"the retry placed revision 2's bytes under revision 1's row"
);
let manifest = Manifest::from_path(&package.storage, &package.installed_manifest()).await?;
let row = manifest
.get_record(&PathBuf::from("changes.txt"))
.expect("revision 1 has changes.txt");
assert!(
!package
.storage
.exists(package.domain_paths.object(row.hash.digest()))
.await,
"revision 2's bytes are in the store under revision 1's hash"
);
assert!(package.status(lineage).await?.changes.is_empty());
Ok(())
}