use corundum_framework_update::{InstallMethod, Release, ReleaseSource, advice, asset_name};
use semver::Version;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RowState {
Checking,
UpToDate {
latest: Version,
},
Newer {
release: Release,
},
NotHere {
release: Release,
advice: String,
},
Working {
release: Release,
step: Step,
},
Done {
version: Version,
},
RestartByHand {
version: Version,
},
Failed {
message: String,
retry: Retry,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Step {
Downloading,
Restarting,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Retry {
Check,
Update(Release),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RestartOutcome {
Restarted,
ByHand,
Failed(String),
}
#[derive(Clone, Debug)]
pub(crate) enum Event {
Checked {
found: Result<Release, String>,
method: InstallMethod,
has_asset: bool,
},
#[cfg_attr(not(feature = "self-update"), allow(dead_code))]
Downloaded(Result<(), String>),
#[cfg_attr(not(feature = "self-update"), allow(dead_code))]
Restarted(RestartOutcome),
Status(Result<corundum_framework_update::PackageStatus, String>),
Applied(Result<(), String>),
}
pub(crate) fn on_check(
current: Option<&Version>,
method: &InstallMethod,
has_asset: bool,
can_install: bool,
source: &ReleaseSource,
found: Result<Release, String>,
) -> RowState {
let release = match found {
Ok(release) => release,
Err(message) => {
return RowState::Failed {
message,
retry: Retry::Check,
};
}
};
if current.is_some_and(|current| release.version <= *current) {
return RowState::UpToDate {
latest: release.version,
};
}
if *method == InstallMethod::Release && can_install && has_asset {
return RowState::Newer { release };
}
let advice = advice(source, &release, method);
RowState::NotHere { release, advice }
}
pub(crate) fn on_event(state: RowState, event: Event) -> RowState {
match (state, event) {
(
RowState::Working {
release,
step: Step::Downloading,
},
Event::Downloaded(result),
) => match result {
Ok(()) => RowState::Working {
release,
step: Step::Restarting,
},
Err(message) => RowState::Failed {
message,
retry: Retry::Update(release),
},
},
(
RowState::Working {
release,
step: Step::Restarting,
},
Event::Restarted(outcome),
) => match outcome {
RestartOutcome::Restarted => RowState::Done {
version: release.version,
},
RestartOutcome::ByHand => RowState::RestartByHand {
version: release.version,
},
RestartOutcome::Failed(message) => RowState::Failed {
message,
retry: Retry::Check,
},
},
(state, _) => state,
}
}
pub(crate) fn has_asset(release: &Release, base: &str) -> bool {
let name = asset_name(base);
release.assets.iter().any(|asset| asset.name == name)
}
#[cfg(test)]
mod tests {
use super::*;
use corundum_framework_update::{Asset, BinstallSource, StoreKind};
fn v(s: &str) -> Version {
s.parse().unwrap()
}
fn release(version: &str) -> Release {
Release {
version: v(version),
url: "https://example.test/r".to_owned(),
assets: vec![Asset {
name: asset_name("app"),
download_url: "https://example.test/a".to_owned(),
}],
}
}
fn source() -> ReleaseSource {
ReleaseSource::github("o", "r").cargo_crate("app-crate")
}
fn check(
current: Option<&str>,
method: InstallMethod,
has_asset: bool,
can_install: bool,
found: Result<Release, String>,
) -> RowState {
let current = current.map(v);
on_check(
current.as_ref(),
&method,
has_asset,
can_install,
&source(),
found,
)
}
#[test]
fn newer_release_is_offered() {
let state = check(
Some("1.0.0"),
InstallMethod::Release,
true,
true,
Ok(release("1.1.0")),
);
assert_eq!(
state,
RowState::Newer {
release: release("1.1.0")
}
);
}
#[test]
fn same_or_older_is_up_to_date() {
for latest in ["1.0.0", "0.9.0"] {
let state = check(
Some("1.0.0"),
InstallMethod::Release,
true,
true,
Ok(release(latest)),
);
assert_eq!(state, RowState::UpToDate { latest: v(latest) });
}
}
#[test]
fn unknown_current_offers_the_release() {
let state = check(
None,
InstallMethod::Release,
true,
true,
Ok(release("1.0.0")),
);
assert!(matches!(state, RowState::Newer { .. }));
}
#[test]
fn other_install_methods_get_advice() {
let git = InstallMethod::Binstall {
crate_name: "app-crate".to_owned(),
source: BinstallSource::Git("https://github.com/o/r".to_owned()),
};
for method in [
InstallMethod::Cargo,
git,
InstallMethod::Store(StoreKind::Flatpak),
InstallMethod::Velopack,
] {
let r = release("1.1.0");
let want = advice(&source(), &r, &method);
let state = check(Some("1.0.0"), method, true, true, Ok(r.clone()));
assert_eq!(
state,
RowState::NotHere {
release: r,
advice: want
}
);
}
let git = InstallMethod::Binstall {
crate_name: "app-crate".to_owned(),
source: BinstallSource::Git("https://github.com/o/r".to_owned()),
};
let RowState::NotHere { advice, .. } =
check(Some("1.0.0"), git, true, true, Ok(release("1.1.0")))
else {
panic!("not NotHere");
};
assert!(advice.contains("cargo binstall --git https://github.com/o/r app-crate"));
}
#[test]
fn missing_asset_names_the_release_page() {
let state = check(
Some("1.0.0"),
InstallMethod::Release,
false,
true,
Ok(release("1.1.0")),
);
let RowState::NotHere { advice, .. } = state else {
panic!("not NotHere");
};
assert!(advice.contains("https://example.test/r"));
}
#[test]
fn without_self_update_says_to_download() {
let state = check(
Some("1.0.0"),
InstallMethod::Release,
true,
false,
Ok(release("1.1.0")),
);
let RowState::NotHere { advice, .. } = state else {
panic!("not NotHere");
};
assert!(advice.contains("download it from https://example.test/r"));
}
#[test]
fn failed_check_retries_the_check() {
let state = check(
Some("1.0.0"),
InstallMethod::Release,
true,
true,
Err("down".into()),
);
assert_eq!(
state,
RowState::Failed {
message: "down".into(),
retry: Retry::Check
}
);
}
fn working(step: Step) -> RowState {
RowState::Working {
release: release("1.1.0"),
step,
}
}
#[test]
fn failed_download_retries_the_update() {
let state = on_event(
working(Step::Downloading),
Event::Downloaded(Err("disk".into())),
);
assert_eq!(
state,
RowState::Failed {
message: "disk".into(),
retry: Retry::Update(release("1.1.0"))
}
);
}
#[test]
fn download_then_restart() {
let state = on_event(working(Step::Downloading), Event::Downloaded(Ok(())));
assert_eq!(state, working(Step::Restarting));
}
#[test]
fn restart_outcomes() {
let done = on_event(
working(Step::Restarting),
Event::Restarted(RestartOutcome::Restarted),
);
assert_eq!(
done,
RowState::Done {
version: v("1.1.0")
}
);
let by_hand = on_event(
working(Step::Restarting),
Event::Restarted(RestartOutcome::ByHand),
);
assert_eq!(
by_hand,
RowState::RestartByHand {
version: v("1.1.0")
}
);
let failed = on_event(
working(Step::Restarting),
Event::Restarted(RestartOutcome::Failed("no".into())),
);
assert_eq!(
failed,
RowState::Failed {
message: "no".into(),
retry: Retry::Check
}
);
}
#[test]
fn unexpected_events_change_nothing() {
let cases = [
(RowState::Checking, Event::Downloaded(Ok(()))),
(
working(Step::Downloading),
Event::Restarted(RestartOutcome::Restarted),
),
(
working(Step::Downloading),
Event::Checked {
found: Ok(release("2.0.0")),
method: InstallMethod::Release,
has_asset: true,
},
),
(
working(Step::Restarting),
Event::Checked {
found: Err("x".into()),
method: InstallMethod::Release,
has_asset: false,
},
),
];
for (state, event) in cases {
assert_eq!(on_event(state.clone(), event), state);
}
}
#[test]
fn cargo_with_equal_version_is_up_to_date() {
let state = check(
Some("1.0.0"),
InstallMethod::Cargo,
true,
true,
Ok(release("1.0.0")),
);
assert_eq!(state, RowState::UpToDate { latest: v("1.0.0") });
}
#[test]
fn has_asset_matches_the_platform_file() {
assert!(has_asset(&release("1.0.0"), "app"));
assert!(!has_asset(&release("1.0.0"), "other"));
}
}