feat(plugin): gate dependency mismatches

This commit is contained in:
Hakan Seven 2026-08-25 08:12:13 +03:00
commit 1e1eeea527
8 changed files with 438 additions and 46 deletions

2
Cargo.lock generated
View file

@ -72,7 +72,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
[[package]]
name = "acadrust"
version = "0.4.1"
source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=1fa0a5e#1fa0a5e22f53dd45c6b0b9d8c63f7fc7730a3a8f"
source = "git+https://git@github.com/HakanSeven12/cadcodec.git?rev=5b2ae66#5b2ae66d0bd7b0da2d13392d3c3332f8a39caa1f"
dependencies = [
"ahash 0.8.12",
"anyhow",

View file

@ -27,7 +27,7 @@ glam = { version = "0.33", features = ["bytemuck"] }
rfd = "0.17"
clap = { version = "4", features = ["derive"] }
env_logger = "0.11"
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "1fa0a5e", features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", features = ["serde"] }
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "b2b1d4b", features = ["acis", "offset"] }
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
flate2 = "1"
@ -61,7 +61,7 @@ iced_core = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aa
iced_widget = { git = "https://github.com/iced-rs/iced.git", rev = "23604ff22ab0aad9e00b9327cb7b8546ed84db39" }
[patch."https://github.com/HakanSeven12/cadcodec.git"]
acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "1fa0a5e" }
acadrust = { git = "https://git@github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
ocs_plugin_api = { path = "crates/ocs_plugin_api", features = ["host"] }

View file

@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
# `HostApi` runtime surface. The default crate stays dependency-free so engine
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", optional = true, features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", optional = true, features = ["serde"] }
# Runtime IPC and serialization (host feature only).
interprocess = { version = "2", optional = true }
@ -37,7 +37,7 @@ serde_json = "1"
serde = { version = "1", features = ["derive"] }
cargo-lock = "11"
# acadrust is scanned at build time to generate the embedded type registry.
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", features = ["serde"] }
[dev-dependencies]
serde_json = "1"

View file

@ -1,5 +1,7 @@
//! Embedded version metadata generated at build time.
use std::sync::OnceLock;
/// The embedded version info as a JSON string.
pub const EMBEDDED_VERSION_INFO_JSON: &str =
include_str!(concat!(env!("OUT_DIR"), "/version_info.json"));
@ -9,6 +11,57 @@ pub fn get_embedded_version_info_json() -> &'static str {
EMBEDDED_VERSION_INFO_JSON
}
#[derive(Debug, Clone)]
struct VersionInfo {
acadrust_source: String,
}
pub const ACADRUST_GATE_API_VERSION: u32 = 4;
/// Returns whether this API version uses the dependency gate.
pub fn uses_acadrust_gate(api_version: u32) -> bool {
api_version >= ACADRUST_GATE_API_VERSION
}
/// Extracts a string from the generated compact JSON.
fn json_string_value(json: &str, key: &str) -> Option<String> {
let pattern = format!(r#""{key}":""#);
let start = json.find(&pattern)? + pattern.len();
let end = json[start..].find('"')?;
Some(json[start..start + end].to_string())
}
fn parsed_version_info() -> &'static VersionInfo {
static INFO: OnceLock<VersionInfo> = OnceLock::new();
INFO.get_or_init(|| VersionInfo {
acadrust_source: json_string_value(EMBEDDED_VERSION_INFO_JSON, "acadrust_source")
.unwrap_or_default(),
})
}
/// Returns the host's full `acadrust` Cargo source.
pub fn host_acadrust_source() -> &'static str {
&parsed_version_info().acadrust_source
}
/// Extracts the full git commit hash from a Cargo source.
pub fn acadrust_source_hash(source: &str) -> Option<&str> {
let hash = source.rsplit('#').next()?;
if hash.len() == 40 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
Some(hash)
} else {
None
}
}
/// Returns whether two Cargo sources use the same commit.
pub fn acadrust_sources_compatible(a: &str, b: &str) -> bool {
match (acadrust_source_hash(a), acadrust_source_hash(b)) {
(Some(ha), Some(hb)) => ha.eq_ignore_ascii_case(hb),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -19,7 +72,7 @@ mod tests {
assert!(!json.is_empty());
let value: serde_json::Value = serde_json::from_str(json).expect("valid JSON");
assert!(value.get("ocs_version").is_some());
assert!(value.get("acadrust_version").is_some());
assert!(value.get("acadrust_source").is_some());
}
#[test]
@ -54,4 +107,57 @@ mod tests {
let acadrust = value["acadrust_version"].as_str().expect("acadrust_version string");
assert_eq!(acadrust.split('.').count(), 3);
}
#[test]
fn host_acadrust_source_is_non_empty() {
assert!(!host_acadrust_source().is_empty());
}
#[test]
fn dependency_gate_starts_at_api_v4() {
assert!(!uses_acadrust_gate(3));
assert!(uses_acadrust_gate(4));
assert!(uses_acadrust_gate(5));
}
#[test]
fn extracts_full_hash_from_cargo_source() {
let src = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
assert_eq!(
acadrust_source_hash(src),
Some("94df2c3f87fa051b16ffc3923f80e9247c85c5fd")
);
}
#[test]
fn rejects_malformed_or_missing_hash() {
assert!(acadrust_source_hash("").is_none());
assert!(acadrust_source_hash("registry+https://crates.io").is_none());
assert!(acadrust_source_hash("git+https://github.com/foo/bar.git#short").is_none());
assert!(
acadrust_source_hash("git+https://github.com/foo/bar.git#gggggggggggggggggggggggggggggggggggggggg")
.is_none()
);
}
#[test]
fn source_comparison_matches_full_hashes() {
let a = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
let b = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
assert!(acadrust_sources_compatible(a, b));
}
#[test]
fn source_comparison_detects_mismatch() {
let a = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
let b = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb";
assert!(!acadrust_sources_compatible(a, b));
}
#[test]
fn source_comparison_case_insensitive() {
let a = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd";
let b = "git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94DF2C3F87FA051B16FFC3923F80E9247C85C5FD";
assert!(acadrust_sources_compatible(a, b));
}
}

View file

@ -8,7 +8,7 @@ publish = false
crate-type = ["cdylib"]
[dependencies]
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "94df2c3", features = ["serde"] }
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "5b2ae66", features = ["serde"] }
bincode = "1.3"
serde = { version = "1", features = ["derive"] }
console_error_panic_hook = "0.1"

View file

@ -30,6 +30,12 @@ pub struct RegistryEntry {
pub struct ReleaseInfo {
pub tag: String,
pub api_version: u32,
/// Full `acadrust` git source used by the release.
pub acadrust_source: Option<String>,
/// Whether `[opencad]` declares `acadrust_source`.
pub acadrust_declared: bool,
/// Whether the release matches this host.
pub acadrust_compatible: bool,
}
/// An add-on package found on disk (not necessarily loaded or compatible).
@ -44,6 +50,10 @@ pub struct ExternalPlugin {
/// an older `plugin.toml` does not declare `repository`.
pub repository: Option<String>,
pub api_version: u32,
/// Full `acadrust` git source used by the plugin.
pub acadrust_source: Option<String>,
/// Whether `[opencad]` declares `acadrust_source`.
pub acadrust_declared: bool,
pub ribbon_order: i32,
pub command_prefixes: Vec<String>,
/// The package directory under the plugins folder.
@ -58,11 +68,27 @@ impl ExternalPlugin {
ocs_plugin_api::manifest::host_accepts_plugin_version(self.api_version)
}
/// True when the package can be loaded today: compatible API *and* a native
/// library present for this platform.
/// Returns whether the package's dependency fingerprint matches the host.
pub fn acadrust_compatible(&self) -> bool {
if !ocs_plugin_api::version_info::uses_acadrust_gate(self.api_version) {
return true;
}
if !self.acadrust_declared {
return true;
}
match self.acadrust_source.as_deref() {
None | Some("") => false,
Some(source) => ocs_plugin_api::version_info::acadrust_sources_compatible(
source,
ocs_plugin_api::version_info::host_acadrust_source(),
),
}
}
/// Returns whether the package can be loaded.
#[allow(dead_code)] // plugin-host surface (issue #100); not yet wired
pub fn loadable(&self) -> bool {
self.api_compatible() && self.lib_present
self.api_compatible() && self.acadrust_compatible() && self.lib_present
}
}
@ -176,12 +202,21 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
let mut description = String::new();
let mut repository = None;
let mut api_version: u32 = 0;
let mut acadrust_source: Option<String> = None;
let mut acadrust_declared = false;
let mut ribbon_order: i32 = 0;
let mut command_prefixes: Vec<String> = Vec::new();
let mut section = None;
for raw in text.lines() {
let line = raw.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with('[') {
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some(header) = line.strip_prefix('[') {
section = header
.find(']')
.map(|end| header[..end].trim());
continue;
}
let Some((key, value)) = line.split_once('=') else {
@ -196,6 +231,11 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
"description" => description = unquote(value),
"repository" => repository = normalize_repository(&unquote(value)),
"api_version" => api_version = value.parse().unwrap_or(0),
"acadrust_source" if section == Some("opencad") => {
acadrust_declared = true;
let v = unquote(value);
acadrust_source = if v.is_empty() { None } else { Some(v) };
}
"ribbon_order" => ribbon_order = value.parse().unwrap_or(0),
"command_prefixes" => command_prefixes = parse_string_array(value),
_ => {}
@ -209,6 +249,8 @@ pub(crate) fn parse_plugin_toml(text: &str) -> Option<ExternalPlugin> {
description,
repository,
api_version,
acadrust_source,
acadrust_declared,
ribbon_order,
command_prefixes,
dir: PathBuf::new(),
@ -298,7 +340,36 @@ mod loader {
manager.set_notification_handler(v4_support::notification_handler());
let mut out = Vec::new();
for d in &discovered {
if !d.api_compatible() || !d.lib_present {
if !d.api_compatible() {
continue;
}
if !d.lib_present {
continue;
}
if ocs_plugin_api::version_info::uses_acadrust_gate(d.api_version)
&& d.acadrust_declared
&& d.acadrust_source.is_none()
{
eprintln!(
"[plugin] {} declares acadrust metadata but has no fingerprint; cannot verify compatibility",
d.id
);
}
if !d.acadrust_compatible() {
let host_src = ocs_plugin_api::version_info::host_acadrust_source();
let plugin_hash = d
.acadrust_source
.as_deref()
.and_then(ocs_plugin_api::version_info::acadrust_source_hash)
.unwrap_or("unknown");
let host_hash = ocs_plugin_api::version_info::acadrust_source_hash(host_src)
.unwrap_or("unknown");
out.push((
d.id.clone(),
Err(format!(
"Plugin built for acadrust @{plugin_hash}, but this host uses @{host_hash}"
)),
));
continue;
}
let Some(path) = lib_file(&d.dir) else {
@ -412,6 +483,121 @@ xdata_apps = ["MYPLUGIN_RECORD"]
assert!(!p.loadable());
}
#[test]
fn undeclared_acadrust_falls_back_to_api_gate() {
let toml = r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 4
"#;
let p = parse_plugin_toml(toml).expect("parsed");
assert!(!p.acadrust_declared);
assert!(p.acadrust_source.is_none());
assert!(p.acadrust_compatible(), "undeclared acadrust is treated as compatible");
}
#[test]
fn declared_empty_acadrust_source_is_incompatible() {
let toml = r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 4
[opencad]
acadrust_source = ""
"#;
let p = parse_plugin_toml(toml).expect("parsed");
assert!(p.acadrust_declared);
assert!(p.acadrust_source.is_none());
assert!(!p.acadrust_compatible(), "declared but empty source is incompatible");
assert!(!p.loadable());
}
#[test]
fn acadrust_source_outside_opencad_is_ignored() {
let toml = r#"
[plugin]
id = "opencad.test"
api_version = 4
acadrust_source = "0123456789012345678901234567890123456789"
"#;
let p = parse_plugin_toml(toml).expect("parsed");
assert!(!p.acadrust_declared);
assert!(p.acadrust_source.is_none());
}
#[test]
fn acadrust_mismatch_detected() {
let host = ocs_plugin_api::version_info::host_acadrust_source();
let other = if host.contains("94df2c3") {
"git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb"
} else {
"git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd"
};
let toml = format!(
r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 4
[opencad]
acadrust_source = "{other}"
"#
);
let p = parse_plugin_toml(&toml).expect("parsed");
assert!(p.acadrust_declared);
assert!(!p.acadrust_compatible(), "mismatched acadrust fingerprint should be incompatible");
assert!(!p.loadable());
}
#[test]
fn acadrust_match_detected() {
let host = ocs_plugin_api::version_info::host_acadrust_source();
let toml = format!(
r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 4
[opencad]
acadrust_source = "{host}"
"#
);
let p = parse_plugin_toml(&toml).expect("parsed");
assert!(p.acadrust_declared);
assert!(p.acadrust_compatible(), "matching acadrust fingerprint should be compatible");
}
#[test]
fn acadrust_gate_only_applies_to_api_v4_and_newer() {
let toml = r#"
[plugin]
id = "opencad.test"
name = "Test"
version = "0.1.0"
api_version = 2
[opencad]
acadrust_source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb"
"#;
let mut p = parse_plugin_toml(toml).expect("parsed");
p.lib_present = true;
assert!(p.acadrust_declared);
assert!(
p.acadrust_compatible(),
"API v2 plugin should bypass acadrust gate"
);
assert!(p.loadable());
}
/// Integration smoke test for the out-of-process plugin path.
/// Set `OCS_TEST_PLUGIN` to the built cdylib path and make sure the
/// `OpenCADStudio` binary is built; the test uses it as the runner host.

View file

@ -219,9 +219,29 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
let manifest_text = download_string(&manifest_asset.url)?;
let manifest = external::parse_plugin_toml(&manifest_text)
.ok_or_else(|| format!("release {} plugin.toml is missing an id", release.tag))?;
let acadrust_source = manifest.acadrust_source.clone();
let acadrust_declared = manifest.acadrust_declared;
let acadrust_compatible = if !ocs_plugin_api::version_info::uses_acadrust_gate(
manifest.api_version,
) {
true
} else if !acadrust_declared {
true
} else {
match acadrust_source.as_deref() {
None | Some("") => false,
Some(source) => ocs_plugin_api::version_info::acadrust_sources_compatible(
source,
ocs_plugin_api::version_info::host_acadrust_source(),
),
}
};
Ok::<_, String>(ReleaseInfo {
tag: release.tag,
api_version: manifest.api_version,
acadrust_source,
acadrust_declared,
acadrust_compatible,
})
})();
match result {
@ -229,6 +249,19 @@ pub fn fetch_release_info(repo: &str) -> Result<Vec<ReleaseInfo>, String> {
Err(error) => last_error = Some(error),
}
}
// Once a repo declares a fingerprint, require it on later API versions.
let any_declared = info.iter().any(|r| r.acadrust_declared);
if any_declared {
for r in &mut info {
if !r.acadrust_declared
&& ocs_plugin_api::version_info::uses_acadrust_gate(r.api_version)
{
r.acadrust_compatible = false;
}
}
}
if info.is_empty() {
Err(last_error.unwrap_or_else(|| "no installable releases found".to_string()))
} else {
@ -283,8 +316,8 @@ fn download_bytes(url: &str) -> Result<Vec<u8>, String> {
}
/// Download and install a release's package into the plugins folder. Verifies
/// the API version from the package's `plugin.toml` first. Returns the plugin
/// id on success.
/// the API version and, when present, the `acadrust` fingerprint from the
/// package's `plugin.toml` first. Returns the plugin id on success.
pub fn install(release: &Release, repository: &str) -> Result<String, String> {
let lib = release.lib_asset().ok_or("no library for this platform")?;
let toml = release.toml_asset().ok_or("release has no plugin.toml")?;
@ -300,6 +333,34 @@ pub fn install(release: &Release, repository: &str) -> Result<String, String> {
));
}
if ocs_plugin_api::version_info::uses_acadrust_gate(manifest.api_version)
&& manifest.acadrust_declared
{
let Some(source) = manifest.acadrust_source.as_deref() else {
return Err(
"Release declares acadrust metadata but has no source; cannot verify ABI compatibility".to_string(),
);
};
if source.is_empty() {
return Err(
"Release declares acadrust metadata but has no source; cannot verify ABI compatibility".to_string(),
);
}
if !ocs_plugin_api::version_info::acadrust_sources_compatible(
source,
ocs_plugin_api::version_info::host_acadrust_source(),
) {
let host_src = ocs_plugin_api::version_info::host_acadrust_source();
let plugin_hash = ocs_plugin_api::version_info::acadrust_source_hash(source)
.unwrap_or("unknown");
let host_hash = ocs_plugin_api::version_info::acadrust_source_hash(host_src)
.unwrap_or("unknown");
return Err(format!(
"Plugin built for acadrust @{plugin_hash}, but this host uses @{host_hash}"
));
}
}
let dir = external::plugins_dir()
.ok_or("cannot locate the plugins folder")?
.join(&manifest.id);

View file

@ -196,17 +196,22 @@ fn trim_version_prefix(value: &str) -> &str {
value.trim_start_matches(|c| c == 'v' || c == 'V')
}
fn newest_update(installed: &str, tags: &[String]) -> Option<String> {
fn newest_update(installed: &str, releases: &[ReleaseInfo]) -> Option<String> {
let installed = semver::Version::parse(trim_version_prefix(installed)).ok()?;
tags.iter()
.filter_map(|tag| {
semver::Version::parse(trim_version_prefix(tag))
releases
.iter()
.filter(|release| {
ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version)
&& release.acadrust_compatible
})
.filter_map(|release| {
semver::Version::parse(trim_version_prefix(&release.tag))
.ok()
.map(|version| (version, tag))
.map(|version| (version, release.tag.clone()))
})
.filter(|(version, _)| version > &installed)
.max_by(|(left, _), (right, _)| left.cmp(right))
.map(|(_, tag)| tag.clone())
.map(|(_, tag)| tag)
}
fn external_card<'a>(
@ -219,12 +224,15 @@ fn external_card<'a>(
selected: bool,
) -> Element<'a, Message> {
let failed_old_api = load_error.is_some() && !p.api_compatible();
let acadrust_mismatch = p.acadrust_declared && !p.acadrust_compatible();
let (status, kind) = if loaded && disabled {
(t!("Disabled"), StatusKind::Muted)
} else if loaded {
(t!("Loaded"), StatusKind::Success)
} else if !p.api_compatible() || failed_old_api {
(t!("API incompatible"), StatusKind::Danger)
} else if acadrust_mismatch {
(t!("Incompatible"), StatusKind::Danger)
} else if load_error.is_some() {
(t!("Load failed"), StatusKind::Danger)
} else if !p.lib_present {
@ -352,11 +360,8 @@ fn install_controls<'a>(
.iter()
.map(|release| release.tag.clone())
.collect::<Vec<_>>();
let selected_api = selected.as_ref().and_then(|selected| {
releases
.iter()
.find(|release| release.tag == *selected)
.map(|release| release.api_version)
let selected_release = selected.as_ref().and_then(|selected| {
releases.iter().find(|release| release.tag == *selected)
});
let picker: Element<'_, Message> = if tags.is_empty() {
text(t!("no releases")).size(11).style(muted_style).into()
@ -367,9 +372,10 @@ fn install_controls<'a>(
.text_size(12)
.into()
};
let action = match selected_api {
Some(api_version)
if ocs_plugin_api::manifest::host_accepts_plugin_version(api_version) =>
let action = match selected_release {
Some(release)
if ocs_plugin_api::manifest::host_accepts_plugin_version(release.api_version)
&& release.acadrust_compatible =>
{
pill_button(
t!("Install"),
@ -842,18 +848,7 @@ pub fn view_window<'a>(
let update_tag = repository
.as_ref()
.and_then(|repo| market.release_tags.get(repo))
.and_then(|releases| {
let compatible_tags = releases
.iter()
.filter(|release| {
ocs_plugin_api::manifest::host_accepts_plugin_version(
release.api_version,
)
})
.map(|release| release.tag.clone())
.collect::<Vec<_>>();
newest_update(&p.version, &compatible_tags)
});
.and_then(|releases| newest_update(&p.version, releases));
list = list.push(external_card(
p,
repository,
@ -984,16 +979,60 @@ pub fn view_web_notice<'a>() -> Element<'a, Message> {
#[cfg(test)]
mod tests {
use super::{newest_update, registry_error_message, repository_display_name};
use crate::plugin::external::ReleaseInfo;
#[test]
fn newest_update_uses_semver_not_release_order() {
let tags = vec![
"v1.4.0".to_string(),
"v2.0.0".to_string(),
"v1.9.9".to_string(),
let releases = vec![
ReleaseInfo {
tag: "v1.4.0".to_string(),
api_version: 4,
acadrust_source: None,
acadrust_declared: false,
acadrust_compatible: true,
},
ReleaseInfo {
tag: "v2.0.0".to_string(),
api_version: 4,
acadrust_source: None,
acadrust_declared: false,
acadrust_compatible: true,
},
ReleaseInfo {
tag: "v1.9.9".to_string(),
api_version: 4,
acadrust_source: None,
acadrust_declared: false,
acadrust_compatible: true,
},
];
assert_eq!(newest_update("1.3.0", &tags).as_deref(), Some("v2.0.0"));
assert_eq!(newest_update("2.0.0", &tags), None);
assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v2.0.0"));
assert_eq!(newest_update("2.0.0", &releases), None);
}
#[test]
fn newest_update_ignores_incompatible_releases() {
let releases = vec![
ReleaseInfo {
tag: "v1.4.0".to_string(),
api_version: 4,
acadrust_source: Some(
"git+https://github.com/HakanSeven12/cadcodec.git?rev=94df2c3#94df2c3f87fa051b16ffc3923f80e9247c85c5fd".to_string(),
),
acadrust_declared: true,
acadrust_compatible: true,
},
ReleaseInfo {
tag: "v2.0.0".to_string(),
api_version: 4,
acadrust_source: Some(
"git+https://github.com/HakanSeven12/cadcodec.git?rev=0908da7#0908da7b6e4f702a6c78359a57f53e2b79cf39eb".to_string(),
),
acadrust_declared: true,
acadrust_compatible: false,
},
];
assert_eq!(newest_update("1.3.0", &releases).as_deref(), Some("v1.4.0"));
}
#[test]