Merge branch 'feat/update-execution-and-provider-contract'
This commit is contained in:
commit
d1b9754c0b
16 changed files with 407 additions and 79 deletions
|
|
@ -18,8 +18,7 @@ pub struct Cli {
|
|||
|
||||
impl Cli {
|
||||
pub fn is_review_update_flow(&self) -> bool {
|
||||
matches!(self.command, Some(Command::Update))
|
||||
|| (self.command.is_none() && self.query.is_none())
|
||||
self.command.is_none() && self.query.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,9 +9,9 @@ use aim_core::app::add::{
|
|||
};
|
||||
use aim_core::app::list::{ListRow, build_list_rows};
|
||||
use aim_core::app::remove::{RemovalResult, remove_registered_app};
|
||||
use aim_core::app::update::build_update_plan;
|
||||
use aim_core::app::update::{build_update_plan, execute_updates};
|
||||
use aim_core::domain::app::AppRecord;
|
||||
use aim_core::domain::update::UpdatePlan;
|
||||
use aim_core::domain::update::{UpdateExecutionResult, UpdatePlan};
|
||||
use aim_core::registry::model::Registry;
|
||||
use aim_core::registry::store::RegistryStore;
|
||||
|
||||
|
|
@ -44,7 +44,15 @@ pub fn dispatch(cli: Cli) -> Result<DispatchResult, DispatchError> {
|
|||
})?;
|
||||
Ok(DispatchResult::Removed(Box::new(removal)))
|
||||
}
|
||||
cli::args::Command::Update => Ok(DispatchResult::UpdatePlan(build_update_plan(&apps)?)),
|
||||
cli::args::Command::Update => {
|
||||
let updates = execute_updates(&apps, &install_home)?;
|
||||
let updated_apps = updates.apps.clone();
|
||||
store.save(&Registry {
|
||||
version: registry.version,
|
||||
apps: updated_apps,
|
||||
})?;
|
||||
Ok(DispatchResult::Updated(Box::new(updates)))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +102,7 @@ pub enum DispatchResult {
|
|||
PendingAdd(Box<AddPlan>),
|
||||
Removed(Box<RemovalResult>),
|
||||
UpdatePlan(UpdatePlan),
|
||||
Updated(Box<UpdateExecutionResult>),
|
||||
Noop,
|
||||
}
|
||||
|
||||
|
|
@ -105,6 +114,7 @@ pub enum DispatchError {
|
|||
RemovePlan(aim_core::app::remove::RemoveRegisteredAppError),
|
||||
Registry(aim_core::registry::store::RegistryStoreError),
|
||||
UpdatePlan(aim_core::app::update::BuildUpdatePlanError),
|
||||
UpdateExecution(aim_core::app::update::ExecuteUpdatesError),
|
||||
}
|
||||
|
||||
impl From<aim_core::app::add::BuildAddPlanError> for DispatchError {
|
||||
|
|
@ -131,6 +141,12 @@ impl From<aim_core::app::update::BuildUpdatePlanError> for DispatchError {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<aim_core::app::update::ExecuteUpdatesError> for DispatchError {
|
||||
fn from(value: aim_core::app::update::ExecuteUpdatesError) -> Self {
|
||||
Self::UpdateExecution(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<aim_core::app::remove::RemoveRegisteredAppError> for DispatchError {
|
||||
fn from(value: aim_core::app::remove::RemoveRegisteredAppError) -> Self {
|
||||
Self::RemovePlan(value)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use aim_core::app::add::AddPlan;
|
||||
use aim_core::domain::update::UpdateExecutionStatus;
|
||||
|
||||
use crate::DispatchResult;
|
||||
|
||||
|
|
@ -15,6 +16,7 @@ pub fn render_dispatch_result(result: &DispatchResult) -> String {
|
|||
DispatchResult::UpdatePlan(plan) => {
|
||||
render_update_summary(plan.items.len(), plan.items.len(), 0)
|
||||
}
|
||||
DispatchResult::Updated(result) => render_updated_apps(result),
|
||||
DispatchResult::Noop => String::new(),
|
||||
}
|
||||
}
|
||||
|
|
@ -88,3 +90,29 @@ fn render_removed_app(removed: &aim_core::app::remove::RemovalResult) -> String
|
|||
format!("{summary}\n{warning_lines}")
|
||||
}
|
||||
}
|
||||
|
||||
fn render_updated_apps(result: &aim_core::domain::update::UpdateExecutionResult) -> String {
|
||||
let mut lines = vec![format!(
|
||||
"updated apps: {}, failed: {}",
|
||||
result.updated_count(),
|
||||
result.failed_count()
|
||||
)];
|
||||
|
||||
for item in &result.items {
|
||||
match &item.status {
|
||||
UpdateExecutionStatus::Updated => lines.push(format!(
|
||||
"updated: {} ({}) {} -> {}",
|
||||
item.display_name,
|
||||
item.stable_id,
|
||||
item.from_version.as_deref().unwrap_or("unknown"),
|
||||
item.to_version.as_deref().unwrap_or("unknown")
|
||||
)),
|
||||
UpdateExecutionStatus::Failed { reason } => lines.push(format!(
|
||||
"failed: {} ({}) {}",
|
||||
item.display_name, item.stable_id, reason
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
lines.join("\n")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use aim_core::registry::model::Registry;
|
||||
use aim_core::registry::store::RegistryStore;
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::PredicateBooleanExt;
|
||||
use predicates::str::contains;
|
||||
use tempfile::tempdir;
|
||||
|
||||
|
|
@ -179,3 +183,49 @@ fn system_request_on_immutable_host_falls_back_to_user_install() {
|
|||
.stdout(contains("installing as user"))
|
||||
.stdout(contains("downgraded to user scope"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_command_applies_updates() {
|
||||
let dir = tempdir().unwrap();
|
||||
let registry_path = dir.path().join("registry.toml");
|
||||
let payload_path = dir
|
||||
.path()
|
||||
.join("install-home/.local/lib/aim/appimages/pingdotgg-t3code.AppImage");
|
||||
let store = RegistryStore::new(registry_path.clone());
|
||||
store
|
||||
.save(&Registry {
|
||||
version: 1,
|
||||
apps: vec![AppRecord {
|
||||
stable_id: "pingdotgg-t3code".to_owned(),
|
||||
display_name: "t3code".to_owned(),
|
||||
source_input: Some("pingdotgg/t3code".to_owned()),
|
||||
source: None,
|
||||
installed_version: Some("0.0.11".to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: Some(InstallMetadata {
|
||||
scope: InstallScope::User,
|
||||
payload_path: None,
|
||||
desktop_entry_path: None,
|
||||
icon_path: None,
|
||||
}),
|
||||
}],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut cmd = Command::cargo_bin("aim").unwrap();
|
||||
|
||||
cmd.arg("update")
|
||||
.env("AIM_REGISTRY_PATH", ®istry_path)
|
||||
.env(FIXTURE_MODE_ENV, "1")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(contains("updated apps: 1"))
|
||||
.stdout(contains("updates found:").not());
|
||||
|
||||
let updated = store.load().unwrap();
|
||||
assert_eq!(updated.apps.len(), 1);
|
||||
assert_eq!(updated.apps[0].stable_id, "pingdotgg-t3code");
|
||||
assert_eq!(updated.apps[0].installed_version.as_deref(), Some("0.0.12"));
|
||||
assert!(payload_path.exists());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
||||
use crate::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||
};
|
||||
use crate::domain::source::SourceRef;
|
||||
|
||||
pub struct CustomJsonAdapter;
|
||||
|
||||
|
|
@ -10,4 +13,12 @@ impl SourceAdapter for CustomJsonAdapter {
|
|||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities::exact_resolution_only()
|
||||
}
|
||||
|
||||
fn normalize(&self, _query: &str) -> Result<SourceRef, AdapterError> {
|
||||
Err(AdapterError::UnsupportedQuery)
|
||||
}
|
||||
|
||||
fn resolve(&self, _source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||
Err(AdapterError::UnsupportedSource)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,32 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, AdapterResolution, SourceAdapter};
|
||||
use crate::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||
};
|
||||
use crate::app::query::resolve_query;
|
||||
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||
|
||||
pub struct DirectUrlAdapter;
|
||||
|
||||
impl DirectUrlAdapter {
|
||||
pub fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, DirectUrlAdapterError> {
|
||||
impl SourceAdapter for DirectUrlAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
"direct-url"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities::exact_resolution_only()
|
||||
}
|
||||
|
||||
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
||||
if source.kind != SourceKind::DirectUrl {
|
||||
return Err(DirectUrlAdapterError::UnsupportedSource);
|
||||
return Err(AdapterError::UnsupportedQuery);
|
||||
}
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||
if source.kind != SourceKind::DirectUrl {
|
||||
return Err(AdapterError::UnsupportedSource);
|
||||
}
|
||||
|
||||
Ok(AdapterResolution {
|
||||
|
|
@ -18,18 +38,3 @@ impl DirectUrlAdapter {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceAdapter for DirectUrlAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
"direct-url"
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities::exact_resolution_only()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum DirectUrlAdapterError {
|
||||
UnsupportedSource,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, AdapterResolution, SourceAdapter};
|
||||
use crate::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||
};
|
||||
use crate::app::query::resolve_query;
|
||||
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||
|
||||
|
|
@ -14,24 +16,6 @@ impl GitHubAdapter {
|
|||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, GitHubAdapterError> {
|
||||
if source.kind != SourceKind::GitHub {
|
||||
return Err(GitHubAdapterError::UnsupportedSource);
|
||||
}
|
||||
|
||||
Ok(AdapterResolution {
|
||||
source: source.clone(),
|
||||
release: ResolvedRelease {
|
||||
version: "latest".to_owned(),
|
||||
prerelease: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub fn normalize(&self, query: &str) -> Result<SourceRef, GitHubAdapterError> {
|
||||
resolve_query(query).map_err(|_| GitHubAdapterError::UnsupportedSource)
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceAdapter for GitHubAdapter {
|
||||
|
|
@ -45,9 +29,27 @@ impl SourceAdapter for GitHubAdapter {
|
|||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum GitHubAdapterError {
|
||||
UnsupportedSource,
|
||||
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
||||
if source.kind != SourceKind::GitHub {
|
||||
return Err(AdapterError::UnsupportedQuery);
|
||||
}
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||
if source.kind != SourceKind::GitHub {
|
||||
return Err(AdapterError::UnsupportedSource);
|
||||
}
|
||||
|
||||
Ok(AdapterResolution {
|
||||
source: source.clone(),
|
||||
release: ResolvedRelease {
|
||||
version: "latest".to_owned(),
|
||||
prerelease: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,11 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, AdapterResolution, SourceAdapter};
|
||||
use crate::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||
};
|
||||
use crate::app::query::resolve_query;
|
||||
use crate::domain::source::{ResolvedRelease, SourceKind, SourceRef};
|
||||
|
||||
pub struct GitLabAdapter;
|
||||
|
||||
impl GitLabAdapter {
|
||||
pub fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, GitLabAdapterError> {
|
||||
if source.kind != SourceKind::GitLab {
|
||||
return Err(GitLabAdapterError::UnsupportedSource);
|
||||
}
|
||||
|
||||
Ok(AdapterResolution {
|
||||
source: source.clone(),
|
||||
release: ResolvedRelease {
|
||||
version: "latest".to_owned(),
|
||||
prerelease: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceAdapter for GitLabAdapter {
|
||||
fn id(&self) -> &'static str {
|
||||
"gitlab"
|
||||
|
|
@ -30,9 +17,27 @@ impl SourceAdapter for GitLabAdapter {
|
|||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum GitLabAdapterError {
|
||||
UnsupportedSource,
|
||||
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError> {
|
||||
let source = resolve_query(query).map_err(|_| AdapterError::UnsupportedQuery)?;
|
||||
if source.kind != SourceKind::GitLab {
|
||||
return Err(AdapterError::UnsupportedQuery);
|
||||
}
|
||||
|
||||
Ok(source)
|
||||
}
|
||||
|
||||
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||
if source.kind != SourceKind::GitLab {
|
||||
return Err(AdapterError::UnsupportedSource);
|
||||
}
|
||||
|
||||
Ok(AdapterResolution {
|
||||
source: source.clone(),
|
||||
release: ResolvedRelease {
|
||||
version: "latest".to_owned(),
|
||||
prerelease: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
||||
use crate::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||
};
|
||||
use crate::domain::source::SourceRef;
|
||||
|
||||
pub struct SourceForgeAdapter;
|
||||
|
||||
|
|
@ -13,4 +16,12 @@ impl SourceAdapter for SourceForgeAdapter {
|
|||
supports_exact_resolution: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize(&self, _query: &str) -> Result<SourceRef, AdapterError> {
|
||||
Err(AdapterError::UnsupportedQuery)
|
||||
}
|
||||
|
||||
fn resolve(&self, _source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||
Err(AdapterError::UnsupportedSource)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use crate::adapters::traits::AdapterCapabilities;
|
||||
use crate::adapters::traits::SourceAdapter;
|
||||
use crate::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||
};
|
||||
use crate::domain::source::SourceRef;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MockAdapter {
|
||||
|
|
@ -24,4 +26,12 @@ impl SourceAdapter for MockAdapter {
|
|||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
self.capabilities
|
||||
}
|
||||
|
||||
fn normalize(&self, _query: &str) -> Result<SourceRef, AdapterError> {
|
||||
Err(AdapterError::UnsupportedQuery)
|
||||
}
|
||||
|
||||
fn resolve(&self, _source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||
Err(AdapterError::UnsupportedSource)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,8 +22,19 @@ pub struct AdapterResolution {
|
|||
pub release: ResolvedRelease,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum AdapterError {
|
||||
UnsupportedQuery,
|
||||
UnsupportedSource,
|
||||
ResolutionFailed(String),
|
||||
}
|
||||
|
||||
pub trait SourceAdapter {
|
||||
fn id(&self) -> &'static str;
|
||||
|
||||
fn capabilities(&self) -> AdapterCapabilities;
|
||||
|
||||
fn normalize(&self, query: &str) -> Result<SourceRef, AdapterError>;
|
||||
|
||||
fn resolve(&self, source: &SourceRef) -> Result<AdapterResolution, AdapterError>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
use crate::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
||||
use crate::adapters::traits::{
|
||||
AdapterCapabilities, AdapterError, AdapterResolution, SourceAdapter,
|
||||
};
|
||||
use crate::domain::source::SourceRef;
|
||||
|
||||
pub struct ZsyncAdapter;
|
||||
|
||||
|
|
@ -10,4 +13,12 @@ impl SourceAdapter for ZsyncAdapter {
|
|||
fn capabilities(&self) -> AdapterCapabilities {
|
||||
AdapterCapabilities::exact_resolution_only()
|
||||
}
|
||||
|
||||
fn normalize(&self, _query: &str) -> Result<SourceRef, AdapterError> {
|
||||
Err(AdapterError::UnsupportedQuery)
|
||||
}
|
||||
|
||||
fn resolve(&self, _source: &SourceRef) -> Result<AdapterResolution, AdapterError> {
|
||||
Err(AdapterError::UnsupportedSource)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
use crate::domain::app::AppRecord;
|
||||
use crate::domain::update::{ChannelPreference, PlannedUpdate, UpdateChannelKind, UpdatePlan};
|
||||
use std::path::Path;
|
||||
|
||||
use crate::app::add::{build_add_plan, install_app};
|
||||
use crate::domain::app::{AppRecord, InstallScope};
|
||||
use crate::domain::update::{
|
||||
ChannelPreference, ExecutedUpdate, PlannedUpdate, UpdateChannelKind, UpdateExecutionResult,
|
||||
UpdateExecutionStatus, UpdatePlan,
|
||||
};
|
||||
|
||||
pub fn build_update_plan(apps: &[AppRecord]) -> Result<UpdatePlan, BuildUpdatePlanError> {
|
||||
Ok(UpdatePlan {
|
||||
|
|
@ -7,9 +13,59 @@ pub fn build_update_plan(apps: &[AppRecord]) -> Result<UpdatePlan, BuildUpdatePl
|
|||
})
|
||||
}
|
||||
|
||||
pub fn execute_updates(
|
||||
apps: &[AppRecord],
|
||||
install_home: &Path,
|
||||
) -> Result<UpdateExecutionResult, ExecuteUpdatesError> {
|
||||
let mut updated_apps = Vec::with_capacity(apps.len());
|
||||
let mut items = Vec::with_capacity(apps.len());
|
||||
|
||||
for app in apps {
|
||||
match execute_update(app, install_home) {
|
||||
Ok(updated) => {
|
||||
let warnings = updated
|
||||
.warnings
|
||||
.iter()
|
||||
.chain(updated.install_outcome.warnings.iter())
|
||||
.cloned()
|
||||
.collect();
|
||||
let record = updated.record;
|
||||
items.push(ExecutedUpdate {
|
||||
stable_id: app.stable_id.clone(),
|
||||
display_name: app.display_name.clone(),
|
||||
from_version: app.installed_version.clone(),
|
||||
to_version: record.installed_version.clone(),
|
||||
warnings,
|
||||
status: UpdateExecutionStatus::Updated,
|
||||
});
|
||||
updated_apps.push(record);
|
||||
}
|
||||
Err(reason) => {
|
||||
items.push(ExecutedUpdate {
|
||||
stable_id: app.stable_id.clone(),
|
||||
display_name: app.display_name.clone(),
|
||||
from_version: app.installed_version.clone(),
|
||||
to_version: app.installed_version.clone(),
|
||||
warnings: Vec::new(),
|
||||
status: UpdateExecutionStatus::Failed { reason },
|
||||
});
|
||||
updated_apps.push(app.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(UpdateExecutionResult {
|
||||
apps: updated_apps,
|
||||
items,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum BuildUpdatePlanError {}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ExecuteUpdatesError {}
|
||||
|
||||
fn plan_update(app: &AppRecord) -> PlannedUpdate {
|
||||
let (selected_channel, selection_reason) = if let Some(strategy) = &app.update_strategy {
|
||||
if strategy.preferred.locator.contains("fail") {
|
||||
|
|
@ -47,3 +103,31 @@ fn plan_update(app: &AppRecord) -> PlannedUpdate {
|
|||
selection_reason,
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_update(
|
||||
app: &AppRecord,
|
||||
install_home: &Path,
|
||||
) -> Result<crate::app::add::InstalledApp, String> {
|
||||
let query = update_query(app).ok_or_else(|| "missing install source".to_owned())?;
|
||||
let requested_scope = app
|
||||
.install
|
||||
.as_ref()
|
||||
.map(|install| install.scope)
|
||||
.unwrap_or(InstallScope::User);
|
||||
let plan = build_add_plan(&query)
|
||||
.map_err(|error| format!("failed to build update plan: {error:?}"))?;
|
||||
|
||||
install_app(&query, &plan, install_home, requested_scope)
|
||||
.map_err(|error| format!("failed to install update: {error:?}"))
|
||||
}
|
||||
|
||||
fn update_query(app: &AppRecord) -> Option<String> {
|
||||
app.source_input.clone().or_else(|| {
|
||||
app.source.as_ref().map(|source| {
|
||||
source
|
||||
.canonical_locator
|
||||
.clone()
|
||||
.unwrap_or_else(|| source.locator.clone())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use crate::domain::app::AppRecord;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
pub enum ParsedMetadataKind {
|
||||
Unknown,
|
||||
|
|
@ -98,3 +100,41 @@ pub struct PlannedUpdate {
|
|||
pub selected_channel: ChannelPreference,
|
||||
pub selection_reason: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct UpdateExecutionResult {
|
||||
pub apps: Vec<AppRecord>,
|
||||
pub items: Vec<ExecutedUpdate>,
|
||||
}
|
||||
|
||||
impl UpdateExecutionResult {
|
||||
pub fn updated_count(&self) -> usize {
|
||||
self.items
|
||||
.iter()
|
||||
.filter(|item| item.status == UpdateExecutionStatus::Updated)
|
||||
.count()
|
||||
}
|
||||
|
||||
pub fn failed_count(&self) -> usize {
|
||||
self.items
|
||||
.iter()
|
||||
.filter(|item| matches!(item.status, UpdateExecutionStatus::Failed { .. }))
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ExecutedUpdate {
|
||||
pub stable_id: String,
|
||||
pub display_name: String,
|
||||
pub from_version: Option<String>,
|
||||
pub to_version: Option<String>,
|
||||
pub warnings: Vec<String>,
|
||||
pub status: UpdateExecutionStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum UpdateExecutionStatus {
|
||||
Updated,
|
||||
Failed { reason: String },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use aim_core::adapters::github::GitHubAdapter;
|
||||
use aim_core::adapters::traits::AdapterCapabilities;
|
||||
use aim_core::adapters::gitlab::GitLabAdapter;
|
||||
use aim_core::adapters::traits::{AdapterCapabilities, SourceAdapter};
|
||||
|
||||
#[test]
|
||||
fn adapter_capabilities_can_report_exact_resolution_only() {
|
||||
|
|
@ -9,10 +10,27 @@ fn adapter_capabilities_can_report_exact_resolution_only() {
|
|||
|
||||
#[test]
|
||||
fn legacy_github_adapter_delegates_to_source_pipeline() {
|
||||
let adapter = GitHubAdapter;
|
||||
let adapter: &dyn SourceAdapter = &GitHubAdapter;
|
||||
|
||||
let result = adapter.normalize("sharkdp/bat").unwrap();
|
||||
|
||||
assert_eq!(result.normalized_kind.as_str(), "github-repository");
|
||||
assert_eq!(result.canonical_locator.as_deref(), Some("sharkdp/bat"));
|
||||
|
||||
let resolution = adapter.resolve(&result).unwrap();
|
||||
assert_eq!(resolution.release.version, "latest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitlab_adapter_normalizes_and_resolves_through_trait() {
|
||||
let adapter: &dyn SourceAdapter = &GitLabAdapter;
|
||||
|
||||
let result = adapter
|
||||
.normalize("https://gitlab.com/example/team/app")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(result.kind.as_str(), "gitlab");
|
||||
|
||||
let resolution = adapter.resolve(&result).unwrap();
|
||||
assert_eq!(resolution.release.version, "latest");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use aim_core::app::update::build_update_plan;
|
||||
use aim_core::domain::app::AppRecord;
|
||||
use aim_core::app::update::{build_update_plan, execute_updates};
|
||||
use aim_core::domain::app::{AppRecord, InstallMetadata, InstallScope};
|
||||
use aim_core::domain::update::{ChannelPreference, UpdateChannelKind, UpdateStrategy};
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn empty_registry_produces_empty_plan() {
|
||||
|
|
@ -61,3 +62,29 @@ fn update_plan_uses_alternate_channel_after_preferred_failure() {
|
|||
);
|
||||
assert_eq!(plan.items[0].selection_reason, "preferred-channel-failed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_update_keeps_previous_app_record() {
|
||||
let install_home = tempdir().unwrap();
|
||||
let previous = AppRecord {
|
||||
stable_id: "legacy-bat".to_owned(),
|
||||
display_name: "Legacy Bat".to_owned(),
|
||||
source_input: None,
|
||||
source: None,
|
||||
installed_version: Some("0.9.0".to_owned()),
|
||||
update_strategy: None,
|
||||
metadata: Vec::new(),
|
||||
install: Some(InstallMetadata {
|
||||
scope: InstallScope::User,
|
||||
payload_path: None,
|
||||
desktop_entry_path: None,
|
||||
icon_path: None,
|
||||
}),
|
||||
};
|
||||
|
||||
let result = execute_updates(std::slice::from_ref(&previous), install_home.path()).unwrap();
|
||||
|
||||
assert_eq!(result.apps, vec![previous]);
|
||||
assert_eq!(result.updated_count(), 0);
|
||||
assert_eq!(result.failed_count(), 1);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue