use std::{collections::BTreeMap, path::Path};
use miette::{Context, IntoDiagnostic, ensure};
use semver::Version;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio::fs;
use url::Url;
use crate::{
ManagedFile,
errors::{DeserializationError, FileNotFound, SerializationError, WriteError},
io::File,
manifest::Manifest,
package::{Package, PackageName},
registry::RegistryUri,
};
mod digest;
pub use digest::{Digest, DigestAlgorithm};
pub const LOCKFILE: &str = "Proto.lock";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum LockedDependency {
Named {
name: PackageName,
},
Qualified {
name: PackageName,
version: Version,
},
}
impl LockedDependency {
pub fn name(&self) -> &PackageName {
match self {
Self::Named { name } | Self::Qualified { name, .. } => name,
}
}
pub fn version(&self) -> Option<&Version> {
match self {
Self::Qualified { version, .. } => Some(version),
Self::Named { .. } => None,
}
}
pub fn named(name: PackageName) -> Self {
Self::Named { name }
}
pub fn qualified(name: PackageName, version: Version) -> Self {
Self::Qualified { name, version }
}
}
impl Serialize for LockedDependency {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
Self::Named { name } => serializer.serialize_str(&format!("{name}")),
Self::Qualified { name, version } => {
serializer.serialize_str(&format!("{name} {version}"))
}
}
}
}
impl<'de> Deserialize<'de> for LockedDependency {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() != 2 {
let name = PackageName::new(parts[0])
.map_err(|e| serde::de::Error::custom(format!("invalid package name: {}", e)))?;
return Ok(Self::Named { name });
}
let name = PackageName::new(parts[0])
.map_err(|e| serde::de::Error::custom(format!("invalid package name: {}", e)))?;
let version = Version::parse(parts[1])
.map_err(|e| serde::de::Error::custom(format!("invalid version: {}", e)))?;
Ok(LockedDependency::Qualified { name, version })
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct LockedPackage {
pub name: PackageName,
pub version: Version,
pub digest: Digest,
pub registry: RegistryUri,
pub repository: String,
pub dependencies: Vec<LockedDependency>,
pub dependants: usize,
}
impl LockedPackage {
pub fn lock(
package: &Package,
registry: RegistryUri,
repository: String,
dependants: usize,
) -> Self {
Self {
name: package.name().to_owned(),
registry,
repository,
digest: package.digest(DigestAlgorithm::SHA256).to_owned(),
version: package.version().to_owned(),
dependencies: package
.manifest
.dependencies
.iter()
.flatten()
.map(|d| LockedDependency::named(d.package.clone()))
.collect(),
dependants,
}
}
pub fn validate(&self, package: &Package) -> miette::Result<()> {
let digest: Digest = DigestAlgorithm::SHA256.digest(&package.tgz);
#[derive(Error, Debug)]
#[error("{property} mismatch - expected {expected}, actual {actual}")]
struct ValidationError {
property: &'static str,
expected: String,
actual: String,
}
ensure!(
&self.name == package.name(),
ValidationError {
property: "name",
expected: self.name.to_string(),
actual: package.name().to_string(),
}
);
ensure!(
&self.version == package.version(),
ValidationError {
property: "version",
expected: self.version.to_string(),
actual: package.version().to_string(),
}
);
ensure!(
self.digest == digest,
ValidationError {
property: "digest",
expected: self.digest.to_string(),
actual: digest.to_string(),
}
);
Ok(())
}
}
#[derive(Serialize, Deserialize)]
struct RawPackageLockfile {
version: u16,
packages: Vec<LockedPackage>,
}
impl RawPackageLockfile {
pub fn v1(packages: Vec<LockedPackage>) -> Self {
Self {
version: 1,
packages,
}
}
}
#[derive(Default, Debug, PartialEq, Clone)]
pub struct PackageLockfile {
packages: BTreeMap<PackageName, LockedPackage>,
}
impl PackageLockfile {
pub fn get(&self, name: &PackageName) -> Option<&LockedPackage> {
self.packages.get(name)
}
pub fn packages(&self) -> impl Iterator<Item = &LockedPackage> {
self.packages.values()
}
}
#[async_trait::async_trait]
impl File for PackageLockfile {
const DEFAULT_PATH: &str = LOCKFILE;
async fn load_from<P>(path: P) -> miette::Result<Self>
where
P: AsRef<Path> + Send + Sync,
{
match fs::read_to_string(path).await {
Ok(contents) => {
let raw: RawPackageLockfile = toml::from_str(&contents)
.into_diagnostic()
.wrap_err(DeserializationError(ManagedFile::Lock))?;
Ok(Self::from_iter(raw.packages))
}
Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {
Err(FileNotFound(LOCKFILE.into()).into())
}
Err(err) => Err(err).into_diagnostic(),
}
}
async fn save_to<P>(&self, path: P) -> miette::Result<()>
where
P: AsRef<Path> + Send + Sync,
{
let mut packages: Vec<_> = self
.packages
.values()
.map(|pkg| {
let mut locked = pkg.clone();
locked.dependencies.sort();
locked
})
.collect();
packages.sort();
let raw = RawPackageLockfile::v1(packages);
let lockfile_path = path.as_ref().join(LOCKFILE);
fs::write(
lockfile_path,
toml::to_string(&raw)
.into_diagnostic()
.wrap_err(SerializationError(ManagedFile::Lock))?
.into_bytes(),
)
.await
.into_diagnostic()
.wrap_err(WriteError(LOCKFILE))
}
}
impl From<PackageLockfile> for Vec<FileRequirement> {
fn from(lock: PackageLockfile) -> Self {
let mut unsorted: Vec<_> = lock.packages.values().collect();
unsorted.sort_by_key(|c| &c.digest);
unsorted.into_iter().map(FileRequirement::from).collect()
}
}
impl TryFrom<Vec<LockedPackage>> for PackageLockfile {
type Error = miette::Error;
fn try_from(locked: Vec<LockedPackage>) -> Result<Self, Self::Error> {
Ok(PackageLockfile::from_iter(locked))
}
}
impl FromIterator<LockedPackage> for PackageLockfile {
fn from_iter<I: IntoIterator<Item = LockedPackage>>(iter: I) -> Self {
Self {
packages: iter
.into_iter()
.map(|locked| (locked.name.clone(), locked))
.collect(),
}
}
}
#[derive(Serialize, Deserialize)]
struct RawWorkspaceLockfile {
version: u16,
packages: Vec<LockedPackage>,
}
impl RawWorkspaceLockfile {
pub fn v1(packages: Vec<LockedPackage>) -> Self {
Self {
version: 1,
packages,
}
}
}
#[derive(Debug, PartialEq, Clone, Default)]
pub struct WorkspaceLockfile {
packages: BTreeMap<(PackageName, Version), LockedPackage>,
}
impl WorkspaceLockfile {
pub fn get(&self, name: &PackageName, version: &Version) -> Option<&LockedPackage> {
self.packages.get(&(name.clone(), version.clone()))
}
pub fn packages(&self) -> impl Iterator<Item = &LockedPackage> {
self.packages.values()
}
}
#[async_trait::async_trait]
impl File for WorkspaceLockfile {
const DEFAULT_PATH: &str = LOCKFILE;
async fn load_from<P>(path: P) -> miette::Result<Self>
where
P: AsRef<Path> + Send + Sync,
{
let path = path.as_ref();
let resolved = if !path.is_file() {
path.join(Self::DEFAULT_PATH)
} else {
path.to_path_buf()
};
match fs::read_to_string(resolved).await {
Ok(contents) => {
let raw: RawWorkspaceLockfile = toml::from_str(&contents)
.into_diagnostic()
.wrap_err(DeserializationError(ManagedFile::Lock))?;
Ok(Self::from_iter(raw.packages))
}
Err(err) if matches!(err.kind(), std::io::ErrorKind::NotFound) => {
Err(FileNotFound(LOCKFILE.into()).into())
}
Err(err) => Err(err).into_diagnostic(),
}
}
async fn save_to<P>(&self, path: P) -> miette::Result<()>
where
P: AsRef<Path> + Send + Sync,
{
let mut packages: Vec<_> = self
.packages
.values()
.map(|pkg| {
let mut locked = pkg.clone();
locked.dependencies.sort();
locked
})
.collect();
packages.sort();
let raw = RawWorkspaceLockfile::v1(packages);
let lockfile_path = path.as_ref().join(LOCKFILE);
fs::write(
lockfile_path,
toml::to_string(&raw)
.into_diagnostic()
.wrap_err(SerializationError(ManagedFile::Lock))?
.into_bytes(),
)
.await
.into_diagnostic()
.wrap_err(WriteError(LOCKFILE))
}
}
impl FromIterator<LockedPackage> for WorkspaceLockfile {
fn from_iter<I: IntoIterator<Item = LockedPackage>>(iter: I) -> Self {
Self {
packages: iter
.into_iter()
.map(|locked| ((locked.name.clone(), locked.version.clone()), locked))
.collect(),
}
}
}
impl From<WorkspaceLockfile> for Vec<FileRequirement> {
fn from(lock: WorkspaceLockfile) -> Self {
let mut unsorted: Vec<_> = lock.packages.values().collect();
unsorted.sort_by_key(|c| &c.digest);
unsorted.into_iter().map(FileRequirement::from).collect()
}
}
#[derive(Debug, Clone)]
pub enum Lockfile {
Package(PackageLockfile),
Workspace(WorkspaceLockfile),
}
impl Lockfile {
pub fn get(&self, name: &PackageName, version: &Version) -> Option<FileRequirement> {
match self {
Self::Package(lock) => lock
.get(name)
.filter(|p| p.version == *version)
.map(FileRequirement::from),
Self::Workspace(lock) => lock.get(name, version).map(FileRequirement::from),
}
}
pub fn packages(&self) -> impl Iterator<Item = &LockedPackage> {
let pkgs: Vec<&LockedPackage> = match self {
Self::Package(pkg) => pkg.packages().collect(),
Self::Workspace(wrk) => wrk.packages().collect(),
};
pkgs.into_iter()
}
pub async fn load_from_or_infer(path: impl AsRef<Path>) -> miette::Result<Self> {
let path = path.as_ref();
let cwd = if path.is_dir() {
path
} else {
path.parent().ok_or(miette::miette!(
"Current working directory does not have a basename"
))?
};
let manifest = Manifest::load_from(cwd).await.wrap_err(miette::miette!(
"Failed to infer lockfile format, no manifest found in cwd {}",
cwd.display()
))?;
let lock = if manifest.to_package_manifest().is_ok() {
PackageLockfile::load_from_or_default(path)
.await
.map(Self::Package)?
} else {
WorkspaceLockfile::load_from_or_default(path)
.await
.map(Self::Workspace)?
};
lock.save_to(path).await?;
Ok(lock)
}
pub fn is_package_lockfile(&self) -> bool {
match self {
Self::Package(_) => true,
Self::Workspace(_) => false,
}
}
pub fn is_workspace_lockfile(&self) -> bool {
match self {
Self::Package(_) => false,
Self::Workspace(_) => true,
}
}
pub fn into_package_lockfile(self) -> miette::Result<PackageLockfile> {
match self {
Self::Package(p) => Ok(p),
Self::Workspace(_) => Err(miette::miette!(
"A package lockfile was expected but a workspace lockfile was found"
)),
}
}
pub fn into_workspace_lockfile(self) -> miette::Result<WorkspaceLockfile> {
match self {
Self::Workspace(w) => Ok(w),
Self::Package(_) => Err(miette::miette!(
"A workspace lockfile was expected but a package lockfile was found"
)),
}
}
}
#[async_trait::async_trait]
impl File for Lockfile {
const DEFAULT_PATH: &str = LOCKFILE;
async fn load_from<P>(path: P) -> miette::Result<Self>
where
P: AsRef<Path> + Send + Sync,
{
let path = path.as_ref();
let path = if !path.is_file() {
path.join(Self::DEFAULT_PATH)
} else {
path.to_path_buf()
};
let plock = PackageLockfile::load_from(&path).await.map(Self::Package);
let wlock = WorkspaceLockfile::load_from(&path)
.await
.map(Self::Workspace);
wlock.or(plock)
}
async fn save_to<P>(&self, path: P) -> miette::Result<()>
where
P: AsRef<Path> + Send + Sync,
{
match self {
Self::Package(plock) => plock.save_to(path).await,
Self::Workspace(wlock) => wlock.save_to(path).await,
}
}
}
impl From<Lockfile> for Vec<FileRequirement> {
fn from(value: Lockfile) -> Self {
match value {
Lockfile::Package(plock) => plock.into(),
Lockfile::Workspace(wlock) => wlock.into(),
}
}
}
impl TryFrom<Vec<LockedPackage>> for WorkspaceLockfile {
type Error = miette::Report;
fn try_from(locked_packages: Vec<LockedPackage>) -> Result<Self, Self::Error> {
use std::collections::BTreeMap;
let mut workspace_packages: BTreeMap<(PackageName, semver::Version), LockedPackage> =
BTreeMap::new();
for locked in locked_packages {
let key = (locked.name.clone(), locked.version.clone());
workspace_packages
.entry(key)
.and_modify(|existing| {
existing.dependants += locked.dependants;
if existing.registry != locked.registry {
tracing::warn!(
"registry mismatch for {}@{}: {} vs {}. Using first seen.",
locked.name,
locked.version,
existing.registry,
locked.registry
);
}
if existing.digest != locked.digest {
tracing::warn!(
"digest mismatch for {}@{}: {} vs {}. Using first seen.",
locked.name,
locked.version,
existing.digest,
locked.digest
);
}
if existing.dependencies != locked.dependencies {
tracing::warn!(
"dependencies mismatch for {}@{}: {:?} vs {:?}. Using first seen.",
locked.name,
locked.version,
existing.dependencies,
locked.dependencies
);
}
})
.or_insert(locked);
}
Ok(Self::from_iter(workspace_packages.into_values()))
}
}
#[derive(Serialize, Clone, PartialEq, Eq)]
pub struct FileRequirement {
pub(crate) package: PackageName,
pub(crate) url: Url,
pub(crate) digest: Digest,
}
impl FileRequirement {
pub fn url(&self) -> &Url {
&self.url
}
pub fn new(
url: &RegistryUri,
repository: &String,
name: &PackageName,
version: &Version,
digest: &Digest,
) -> Self {
let mut url = url.clone();
let new_path = format!(
"{}/{}/{}/{}-{}.tgz",
url.path(),
repository,
name,
name,
version
);
url.set_path(&new_path);
Self {
package: name.to_owned(),
url: url.into(),
digest: digest.clone(),
}
}
}
impl From<LockedPackage> for FileRequirement {
fn from(package: LockedPackage) -> Self {
Self::new(
&package.registry,
&package.repository,
&package.name,
&package.version,
&package.digest,
)
}
}
impl From<&LockedPackage> for FileRequirement {
fn from(package: &LockedPackage) -> Self {
Self::new(
&package.registry,
&package.repository,
&package.name,
&package.version,
&package.digest,
)
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, str::FromStr};
use semver::Version;
use crate::{io::File, package::PackageName, registry::RegistryUri};
use super::{
Digest, DigestAlgorithm, FileRequirement, LockedDependency, LockedPackage, PackageLockfile,
WorkspaceLockfile,
};
fn simple_lockfile() -> PackageLockfile {
PackageLockfile {
packages: BTreeMap::from([
(
PackageName::new("package1").unwrap(),
LockedPackage {
name: PackageName::new("package1").unwrap(),
digest: Digest::from_parts(
DigestAlgorithm::SHA256,
"c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353c122",
)
.unwrap(),
registry: RegistryUri::from_str("http://my-registry.com").unwrap(),
repository: "my-repo".to_owned(),
version: Version::new(0, 1, 0),
dependencies: Default::default(),
dependants: 1,
},
),
(
PackageName::new("package2").unwrap(),
LockedPackage {
name: PackageName::new("package2").unwrap(),
digest: Digest::from_parts(
DigestAlgorithm::SHA256,
"c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
)
.unwrap(),
registry: RegistryUri::from_str("http://my-registry.com").unwrap(),
repository: "my-other-repo".to_owned(),
version: Version::new(0, 2, 0),
dependencies: Default::default(),
dependants: 1,
},
),
(
PackageName::new("package3").unwrap(),
LockedPackage {
name: PackageName::new("package3").unwrap(),
digest: Digest::from_parts(
DigestAlgorithm::SHA256,
"c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
)
.unwrap(),
registry: RegistryUri::from_str("http://your-registry.com").unwrap(),
repository: "your-repo".to_owned(),
version: Version::new(0, 2, 0),
dependencies: Default::default(),
dependants: 1,
},
),
(
PackageName::new("package4").unwrap(),
LockedPackage {
name: PackageName::new("package4").unwrap(),
digest: Digest::from_parts(
DigestAlgorithm::SHA256,
"c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
)
.unwrap(),
registry: RegistryUri::from_str("http://your-registry.com").unwrap(),
repository: "your-other-repo".to_owned(),
version: Version::new(0, 2, 0),
dependencies: Default::default(),
dependants: 1,
},
),
]),
}
}
#[test]
fn stable_file_requirement_order() {
let lock = simple_lockfile();
let files: Vec<FileRequirement> = lock.into();
for _ in 0..30 {
let other_files: Vec<FileRequirement> = simple_lockfile().into();
assert!(other_files == files)
}
}
#[tokio::test]
async fn test_exists_at_returns_false_for_nonexistent_file() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let lockfile_path = temp_dir.path().join("Proto.lock");
let exists = PackageLockfile::exists_at(&lockfile_path).await.unwrap();
assert!(!exists);
}
#[tokio::test]
async fn test_exists_at_returns_true_for_existing_file() {
use tempfile::TempDir;
use tokio::fs;
let temp_dir = TempDir::new().unwrap();
let lockfile_path = temp_dir.path().join("Proto.lock");
fs::write(&lockfile_path, "").await.unwrap();
let exists = PackageLockfile::exists_at(&lockfile_path).await.unwrap();
assert!(exists);
}
#[tokio::test]
async fn test_exists_at_accepts_reference_and_owned() {
use std::path::PathBuf;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let lockfile_path = temp_dir.path().join("Proto.lock");
let exists_ref = PackageLockfile::exists_at(&lockfile_path).await.unwrap();
assert!(!exists_ref);
let lockfile_path_owned = PathBuf::from(&lockfile_path);
let exists_owned = PackageLockfile::exists_at(lockfile_path_owned)
.await
.unwrap();
assert!(!exists_owned);
let path_str = lockfile_path.to_str().unwrap();
let exists_str = PackageLockfile::exists_at(path_str).await.unwrap();
assert!(!exists_str);
}
#[tokio::test]
async fn test_read_from_or_default_returns_default_when_file_missing() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let lockfile_path = temp_dir.path().join("Proto.lock");
let lockfile = PackageLockfile::load_from_or_default(&lockfile_path)
.await
.unwrap();
assert_eq!(lockfile.packages.len(), 0);
assert_eq!(lockfile, PackageLockfile::default());
}
#[tokio::test]
async fn test_read_from_or_default_reads_existing_file() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let lockfile_path = temp_dir.path().join("Proto.lock");
let original_lockfile = simple_lockfile();
original_lockfile.save_to(temp_dir.path()).await.unwrap();
let loaded_lockfile = PackageLockfile::load_from_or_default(&lockfile_path)
.await
.unwrap();
assert_eq!(loaded_lockfile.packages.len(), 4);
assert!(
loaded_lockfile
.packages
.contains_key(&PackageName::new("package1").unwrap())
);
assert!(
loaded_lockfile
.packages
.contains_key(&PackageName::new("package2").unwrap())
);
assert!(
loaded_lockfile
.packages
.contains_key(&PackageName::new("package3").unwrap())
);
assert!(
loaded_lockfile
.packages
.contains_key(&PackageName::new("package4").unwrap())
);
}
#[test]
fn test_locked_dependency_serialization() {
let deps = vec![
LockedDependency::qualified(
PackageName::unchecked("remote-lib-a"),
Version::new(1, 5, 0),
),
LockedDependency::qualified(
PackageName::unchecked("remote-lib-b"),
Version::new(2, 0, 1),
),
];
#[derive(serde::Serialize, serde::Deserialize)]
struct TestWrapper {
dependencies: Vec<LockedDependency>,
}
let wrapper = TestWrapper { dependencies: deps };
let serialized = toml::to_string(&wrapper).unwrap();
assert!(serialized.contains("dependencies = ["));
assert!(serialized.contains("\"remote-lib-a 1.5.0\""));
assert!(serialized.contains("\"remote-lib-b 2.0.1\""));
let deserialized: TestWrapper = toml::from_str(&serialized).unwrap();
assert_eq!(deserialized.dependencies.len(), 2);
assert_eq!(
*deserialized.dependencies[0].name(),
PackageName::unchecked("remote-lib-a")
);
assert_eq!(
deserialized.dependencies[0].version().cloned(),
Some(Version::new(1, 5, 0))
);
assert_eq!(
*deserialized.dependencies[1].name(),
PackageName::unchecked("remote-lib-b")
);
assert_eq!(
deserialized.dependencies[1].version().cloned(),
Some(Version::new(2, 0, 1))
);
}
#[test]
fn test_workspace_lockfile_serialization() {
let pkg1 = LockedPackage {
name: PackageName::unchecked("remote-lib-a"),
version: Version::new(1, 0, 0),
registry: RegistryUri::from_str("https://my-registry.com").unwrap(),
repository: "test-repo".to_string(),
digest: Digest::from_parts(
DigestAlgorithm::SHA256,
"c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353c122",
)
.unwrap(),
dependencies: vec![LockedDependency::qualified(
PackageName::unchecked("remote-lib-b"),
Version::new(1, 5, 0),
)],
dependants: 2,
};
let pkg2 = LockedPackage {
name: PackageName::unchecked("remote-lib-b"),
version: Version::new(1, 5, 0),
registry: RegistryUri::from_str("https://my-registry.com").unwrap(),
repository: "test-repo".to_string(),
digest: Digest::from_parts(
DigestAlgorithm::SHA256,
"c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
)
.unwrap(),
dependencies: vec![], dependants: 1,
};
let lockfile = WorkspaceLockfile::from_iter(vec![pkg1, pkg2]);
let serialized = toml::to_string(&super::RawWorkspaceLockfile {
version: 1,
packages: lockfile.packages.values().cloned().collect(),
})
.unwrap();
assert!(serialized.contains("version = 1"));
assert!(serialized.contains("[[packages]]"));
assert!(serialized.contains("name = \"remote-lib-a\""));
assert!(serialized.contains("version = \"1.0.0\""));
assert!(serialized.contains("dependencies = [\"remote-lib-b 1.5.0\"]"));
assert!(serialized.contains("dependants = 2"));
assert!(serialized.contains("name = \"remote-lib-b\""));
assert!(serialized.contains("version = \"1.5.0\""));
assert!(serialized.contains("dependencies = []"));
assert!(serialized.contains("dependants = 1"));
let raw: super::RawWorkspaceLockfile = toml::from_str(&serialized).unwrap();
assert_eq!(raw.version, 1);
assert_eq!(raw.packages.len(), 2);
let restored = WorkspaceLockfile::from_iter(raw.packages);
assert_eq!(restored.packages.len(), 2);
let found = restored.get(
&PackageName::unchecked("remote-lib-a"),
&Version::new(1, 0, 0),
);
assert!(found.is_some());
assert_eq!(found.unwrap().dependencies.len(), 1);
}
#[test]
fn test_workspace_lockfile_supports_multiple_versions() {
let pkg_v1 = LockedPackage {
name: PackageName::unchecked("remote-lib"),
version: Version::new(1, 0, 0),
registry: RegistryUri::from_str("https://my-registry.com").unwrap(),
repository: "test-repo".to_string(),
digest: Digest::from_parts(
DigestAlgorithm::SHA256,
"c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353c122",
)
.unwrap(),
dependencies: vec![],
dependants: 1,
};
let pkg_v2 = LockedPackage {
name: PackageName::unchecked("remote-lib"),
version: Version::new(2, 0, 0),
registry: RegistryUri::from_str("https://my-registry.com").unwrap(),
repository: "test-repo".to_string(),
digest: Digest::from_parts(
DigestAlgorithm::SHA256,
"c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353bce3",
)
.unwrap(),
dependencies: vec![],
dependants: 1,
};
let lockfile = WorkspaceLockfile::from_iter(vec![pkg_v1, pkg_v2]);
assert_eq!(lockfile.packages.len(), 2);
let v1 = lockfile.get(
&PackageName::unchecked("remote-lib"),
&Version::new(1, 0, 0),
);
assert!(v1.is_some());
assert_eq!(v1.unwrap().version, Version::new(1, 0, 0));
let v2 = lockfile.get(
&PackageName::unchecked("remote-lib"),
&Version::new(2, 0, 0),
);
assert!(v2.is_some());
assert_eq!(v2.unwrap().version, Version::new(2, 0, 0));
}
#[test]
fn test_lockfile_package_returns_file_requirement() {
let lockfile = simple_lockfile();
let resolved = super::Lockfile::Package(lockfile);
let result = resolved.get(
&PackageName::new("package1").unwrap(),
&Version::new(0, 1, 0),
);
assert!(result.is_some());
let file_req = result.unwrap();
assert!(file_req.url().as_str().contains("package1"));
let result = resolved.get(
&PackageName::new("package1").unwrap(),
&Version::new(9, 9, 9),
);
assert!(result.is_none());
let result = resolved.get(
&PackageName::new("unknown").unwrap(),
&Version::new(0, 1, 0),
);
assert!(result.is_none());
}
#[test]
fn test_lockfile_workspace_returns_file_requirement() {
let pkg = LockedPackage {
name: PackageName::unchecked("ws-pkg"),
version: Version::new(1, 0, 0),
registry: RegistryUri::from_str("https://registry.example.com").unwrap(),
repository: "repo".to_string(),
digest: Digest::from_parts(
DigestAlgorithm::SHA256,
"c109c6b120c525e6ea7b2db98335d39a3272f572ac86ba7b2d65c765c353c122",
)
.unwrap(),
dependencies: vec![],
dependants: 1,
};
let lockfile = WorkspaceLockfile::from_iter(vec![pkg]);
let resolved = super::Lockfile::Workspace(lockfile);
let result = resolved.get(&PackageName::unchecked("ws-pkg"), &Version::new(1, 0, 0));
assert!(result.is_some());
let result = resolved.get(&PackageName::unchecked("ws-pkg"), &Version::new(2, 0, 0));
assert!(result.is_none());
}
}