Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,20 @@
|
||||
use ely_platform::{AppId, logging};
|
||||
|
||||
#[test]
|
||||
fn log_lines_reach_the_app_file_and_a_second_init_fails() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
logging::init_in(AppId::Doc, dir.path()).unwrap();
|
||||
log::info!("hello from the test");
|
||||
log::logger().flush();
|
||||
|
||||
let text = std::fs::read_to_string(dir.path().join("ely.log")).unwrap();
|
||||
assert!(text.contains("ElyDoc 0.1.0 started"), "{text}");
|
||||
assert!(text.contains("hello from the test"), "{text}");
|
||||
|
||||
let error = logging::init_in(AppId::Mail, dir.path()).unwrap_err();
|
||||
assert!(
|
||||
format!("{error:#}").contains("a logger is already set"),
|
||||
"{error:#}"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//! The system player against a small H.264/AAC clip.
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ely_platform::media::{Player, State};
|
||||
use objc2_foundation::{NSDate, NSRunLoop};
|
||||
|
||||
// The player works through the main run loop.
|
||||
let pump = |until: &dyn Fn() -> bool| {
|
||||
let end = Instant::now() + Duration::from_secs(10);
|
||||
while !until() {
|
||||
assert!(Instant::now() < end, "the player never got there");
|
||||
let soon = NSDate::dateWithTimeIntervalSinceNow(0.02);
|
||||
NSRunLoop::currentRunLoop().runUntilDate(&soon);
|
||||
}
|
||||
};
|
||||
let clip = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../tests/fixtures/media/clip.mp4");
|
||||
let player = Player::open(&clip).expect("opens");
|
||||
pump(&|| player.state() != State::Loading);
|
||||
let State::Ready { seconds, video } = player.state() else {
|
||||
panic!("not ready: {:?}", player.state());
|
||||
};
|
||||
assert!((seconds - 1.0).abs() < 0.2, "{seconds}");
|
||||
assert_eq!(video, Some((160, 90)));
|
||||
player.play();
|
||||
let frames = std::cell::Cell::new(0);
|
||||
pump(&|| {
|
||||
frames.set(frames.get() + usize::from(player.frame().is_some()));
|
||||
player.position() > 0.3 && frames.get() > 0
|
||||
});
|
||||
assert!(player.playing());
|
||||
// At its end it stops; a loop starts over.
|
||||
pump(&|| player.ended());
|
||||
assert_eq!(player.tick(false), Ok(false), "done, nothing to draw");
|
||||
assert_eq!(player.tick(true), Ok(true), "the loop goes on");
|
||||
pump(&|| player.playing() && player.position() < seconds - 0.2);
|
||||
// A pause near the end holds, loop or not.
|
||||
player.pause();
|
||||
player.seek(seconds - 0.02);
|
||||
pump(&|| player.position() >= seconds - 0.05);
|
||||
assert!(!player.ended());
|
||||
assert_eq!(player.tick(true), Ok(false));
|
||||
assert!(!player.playing(), "the pause holds");
|
||||
player.play();
|
||||
player.pause();
|
||||
assert!(Player::open(std::path::Path::new("/no/such/clip.mp4")).is_err());
|
||||
let dir = tempfile::tempdir().expect("a temp dir");
|
||||
let junk = dir.path().join("junk.mp4");
|
||||
std::fs::write(&junk, b"not a video at all").expect("writes");
|
||||
let broken = Player::open(&junk).expect("opens the file");
|
||||
pump(&|| broken.tick(false) != Ok(true));
|
||||
assert!(matches!(broken.tick(false), Err(reason) if !reason.is_empty()));
|
||||
println!("media: plays, shows frames, and fails with reasons");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn main() {
|
||||
println!("SKIP: no system player is built on this platform");
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use ely_platform::AppId::{self, *};
|
||||
|
||||
#[test]
|
||||
fn packaging_table_lists_the_apps_in_prd_order() {
|
||||
let apps: [AppId; 8] = [Doc, Table, Show, Pdf, Mail, Tree, Mark, Agent];
|
||||
let table = include_str!("../../../packaging/apps.tsv");
|
||||
let rows: Vec<Vec<&str>> = table
|
||||
.lines()
|
||||
.map(|line| line.split('\t').collect())
|
||||
.collect();
|
||||
|
||||
assert_eq!(rows.len(), apps.len());
|
||||
for (row, app) in rows.iter().zip(apps) {
|
||||
assert_eq!(row[..2], [app.slug(), app.name()]);
|
||||
assert!(
|
||||
row[2].ends_with(';'),
|
||||
"{} has no desktop categories",
|
||||
app.slug()
|
||||
);
|
||||
// Mail and Agent ship in the second batch.
|
||||
let batch = if matches!(app, Mail | Agent) { "2" } else { "1" };
|
||||
assert_eq!(row[3], batch, "{} is in the wrong batch", app.slug());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::{fs, path::PathBuf, process::Command};
|
||||
|
||||
use ely_platform::{AppId, logging, startup};
|
||||
|
||||
const CHILD: &str = "ELY_STARTUP_TEST_DIR";
|
||||
const NO_LOG: &str = "ELY_STARTUP_TEST_NO_LOG";
|
||||
|
||||
fn rerun(test: &str, dir: &std::path::Path, no_log: bool) -> std::process::Output {
|
||||
let mut command = Command::new(std::env::current_exe().unwrap());
|
||||
command
|
||||
.args(["--exact", test, "--nocapture"])
|
||||
.env(CHILD, dir)
|
||||
.env("ELY_NO_DIALOG", "1");
|
||||
if no_log {
|
||||
command.env(NO_LOG, "1");
|
||||
}
|
||||
command.output().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_startup_failure_is_logged_printed_and_exits_with_one() {
|
||||
const TEST: &str = "a_startup_failure_is_logged_printed_and_exits_with_one";
|
||||
if let Some(dir) = std::env::var_os(CHILD) {
|
||||
logging::init_in(AppId::Doc, &PathBuf::from(dir)).unwrap();
|
||||
startup::fail(AppId::Doc, "no usable GPU");
|
||||
}
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output = rerun(TEST, dir.path(), false);
|
||||
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let log_path = dir.path().join("ely.log");
|
||||
assert!(
|
||||
stderr.contains("ElyDoc could not start") && stderr.contains("no usable GPU"),
|
||||
"{stderr}"
|
||||
);
|
||||
assert!(
|
||||
stderr.contains(&log_path.display().to_string()),
|
||||
"the message does not name the real log: {stderr}"
|
||||
);
|
||||
let log = fs::read_to_string(log_path).unwrap();
|
||||
assert!(log.contains("ElyDoc cannot start: no usable GPU"), "{log}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_logger_the_message_says_there_is_no_log() {
|
||||
const TEST: &str = "without_a_logger_the_message_says_there_is_no_log";
|
||||
if std::env::var_os(NO_LOG).is_some() {
|
||||
startup::fail(AppId::Doc, "logging did not start: read-only home");
|
||||
}
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let output = rerun(TEST, dir.path(), true);
|
||||
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
assert!(stderr.contains("none, logging did not start"), "{stderr}");
|
||||
assert!(!stderr.contains("ely.log"), "{stderr}");
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::{Read, Write},
|
||||
net::{TcpListener, TcpStream},
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ely_platform::{
|
||||
AppId, logging,
|
||||
update::{Check, IDLE_TIMEOUT, Release, check, download, platform, verify_file},
|
||||
};
|
||||
use semver::Version;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Reply {
|
||||
Body(Vec<u8>),
|
||||
Redirect(String),
|
||||
Stall(Arc<AtomicBool>),
|
||||
}
|
||||
|
||||
fn hex_of(bytes: &[u8]) -> String {
|
||||
Sha256::digest(bytes)
|
||||
.iter()
|
||||
.map(|b| format!("{b:02x}"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn payload() -> Vec<u8> {
|
||||
(0..300_000u32).map(|n| (n % 251) as u8).collect()
|
||||
}
|
||||
|
||||
fn respond(mut stream: TcpStream, routes: &[(String, Reply)]) {
|
||||
let mut request = Vec::new();
|
||||
let mut byte = [0u8; 1];
|
||||
while !request.ends_with(b"\r\n\r\n") {
|
||||
if stream.read(&mut byte).unwrap() == 0 {
|
||||
return;
|
||||
}
|
||||
request.push(byte[0]);
|
||||
}
|
||||
let line = String::from_utf8_lossy(&request);
|
||||
let path = line.split_whitespace().nth(1).unwrap_or("/").to_string();
|
||||
let reply = routes
|
||||
.iter()
|
||||
.find(|(route, _)| *route == path)
|
||||
.map(|(_, reply)| reply.clone());
|
||||
let bytes = match reply {
|
||||
Some(Reply::Body(body)) => {
|
||||
let head = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
||||
body.len()
|
||||
);
|
||||
[head.into_bytes(), body].concat()
|
||||
}
|
||||
Some(Reply::Redirect(to)) => format!(
|
||||
"HTTP/1.1 302 Found\r\nLocation: {to}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
|
||||
)
|
||||
.into_bytes(),
|
||||
Some(Reply::Stall(closed)) => {
|
||||
let _ = stream.write_all(
|
||||
b"HTTP/1.1 200 OK\r\nContent-Length: 1000000\r\nConnection: close\r\n\r\n",
|
||||
);
|
||||
let _ = stream.write_all(&[7u8; 1000]);
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(20)))
|
||||
.unwrap();
|
||||
let seen = stream.read(&mut [0u8; 1]);
|
||||
if matches!(seen, Ok(0))
|
||||
|| seen.is_err_and(|error| error.kind() == std::io::ErrorKind::ConnectionReset)
|
||||
{
|
||||
closed.store(true, Ordering::Relaxed);
|
||||
}
|
||||
return;
|
||||
}
|
||||
None => {
|
||||
b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n".to_vec()
|
||||
}
|
||||
};
|
||||
let _ = stream.write_all(&bytes);
|
||||
}
|
||||
|
||||
/// Serves routes locally; `make` gets the base URL.
|
||||
fn serve(make: impl FnOnce(&str) -> Vec<(String, Reply)>) -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let base = format!("http://127.0.0.1:{}", listener.local_addr().unwrap().port());
|
||||
let routes = make(&base);
|
||||
thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
let routes = routes.clone();
|
||||
let stream = stream.unwrap();
|
||||
thread::spawn(move || respond(stream, &routes));
|
||||
}
|
||||
});
|
||||
base
|
||||
}
|
||||
|
||||
fn manifest(version: &str, key: &str, base: &str, sha: &str) -> Reply {
|
||||
Reply::Body(
|
||||
format!(r#"{{"version":"{version}","assets":{{"{key}":{{"url":"{base}/ElyDoc.bin","sha256":"{sha}"}}}}}}"#)
|
||||
.into_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
fn release(base: &str, sha: String) -> Release {
|
||||
Release {
|
||||
version: Version::new(0, 2, 0),
|
||||
url: format!("{base}/ElyDoc.bin"),
|
||||
sha256: sha,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_text<T>(result: anyhow::Result<T>) -> String {
|
||||
format!("{:#}", result.err().expect("an error"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_newer_release_for_this_platform_is_offered_and_an_equal_or_older_one_is_not() {
|
||||
let sha = hex_of(&payload()).to_uppercase();
|
||||
let base = serve(|base| {
|
||||
vec![(
|
||||
"/latest.json".into(),
|
||||
manifest("0.2.0", &platform(), base, &sha),
|
||||
)]
|
||||
});
|
||||
let url = format!("{base}/latest.json");
|
||||
|
||||
let found = check(&Version::new(0, 1, 0), &url).unwrap();
|
||||
assert_eq!(found, Check::Available(release(&base, sha.to_lowercase())));
|
||||
assert_eq!(check(&Version::new(0, 2, 0), &url).unwrap(), Check::Current);
|
||||
assert_eq!(check(&Version::new(0, 3, 0), &url).unwrap(), Check::Current);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_manifest_without_this_platform_a_bad_manifest_and_a_missing_one_all_fail_loudly() {
|
||||
let base = serve(|base| {
|
||||
vec![
|
||||
(
|
||||
"/other.json".into(),
|
||||
manifest("0.2.0", "plan9-mips", base, "00"),
|
||||
),
|
||||
("/bad.json".into(), Reply::Body(b"{not json".to_vec())),
|
||||
]
|
||||
});
|
||||
let current = Version::new(0, 1, 0);
|
||||
|
||||
assert!(
|
||||
error_text(check(¤t, &format!("{base}/other.json"))).contains("has no build for")
|
||||
);
|
||||
assert!(error_text(check(¤t, &format!("{base}/bad.json"))).contains("not valid"));
|
||||
assert!(error_text(check(¤t, &format!("{base}/none.json"))).contains("cannot fetch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_http_to_other_hosts_and_credentials_in_the_url_are_refused_before_any_request() {
|
||||
let current = Version::new(0, 1, 0);
|
||||
|
||||
assert!(error_text(check(¤t, "http://example.com/latest.json")).contains("not https"));
|
||||
let sneaky = "http://127.0.0.1:1@192.168.31.221:9/latest.json";
|
||||
assert!(error_text(check(¤t, sneaky)).contains("carries credentials"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_redirect_is_followed_on_this_machine_and_refused_when_it_leaves_for_plain_http() {
|
||||
let sha = hex_of(&payload());
|
||||
let base = serve(|base| {
|
||||
vec![
|
||||
(
|
||||
"/latest.json".into(),
|
||||
manifest("0.2.0", &platform(), base, &sha),
|
||||
),
|
||||
("/first".into(), Reply::Redirect("/second".into())),
|
||||
("/second".into(), Reply::Redirect("/latest.json".into())),
|
||||
(
|
||||
"/leave".into(),
|
||||
Reply::Redirect("http://example.com/latest.json".into()),
|
||||
),
|
||||
]
|
||||
});
|
||||
let current = Version::new(0, 1, 0);
|
||||
|
||||
let followed = check(¤t, &format!("{base}/first")).unwrap();
|
||||
assert!(matches!(followed, Check::Available(_)), "{followed:?}");
|
||||
assert!(error_text(check(¤t, &format!("{base}/leave"))).contains("not https"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_download_is_verified_reported_and_moved_into_place() {
|
||||
let body = payload();
|
||||
let base = serve(|_| vec![("/ElyDoc.bin".into(), Reply::Body(body.clone()))]);
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let mut seen = Vec::new();
|
||||
|
||||
let path = download(
|
||||
&release(&base, hex_of(&body)),
|
||||
dir.path(),
|
||||
&AtomicBool::new(false),
|
||||
IDLE_TIMEOUT,
|
||||
&mut |done, total| {
|
||||
seen.push((done, total));
|
||||
},
|
||||
)
|
||||
.unwrap()
|
||||
.expect("not cancelled");
|
||||
|
||||
assert_eq!(path, dir.path().join("ElyDoc.bin"));
|
||||
assert_eq!(fs::read(&path).unwrap(), body);
|
||||
assert!(!dir.path().join("ElyDoc.bin.part").exists());
|
||||
assert_eq!(
|
||||
seen.last().copied(),
|
||||
Some((body.len() as u64, Some(body.len() as u64)))
|
||||
);
|
||||
assert!(
|
||||
seen.windows(2).all(|pair| pair[0].0 < pair[1].0),
|
||||
"progress went backwards"
|
||||
);
|
||||
verify_file(&path, &hex_of(&body)).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_wrong_hash_leaves_no_file_behind() {
|
||||
let body = payload();
|
||||
let base = serve(|_| vec![("/ElyDoc.bin".into(), Reply::Body(body.clone()))]);
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let result = download(
|
||||
&release(&base, hex_of(b"other")),
|
||||
dir.path(),
|
||||
&AtomicBool::new(false),
|
||||
IDLE_TIMEOUT,
|
||||
&mut |_, _| {},
|
||||
);
|
||||
|
||||
assert!(error_text(result).contains("the release says"));
|
||||
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 0);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn the_download_error_survives_a_failed_cleanup() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let body = payload();
|
||||
let base = serve(|_| vec![("/ElyDoc.bin".into(), Reply::Body(body.clone()))]);
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let lock = |mode| fs::set_permissions(dir.path(), fs::Permissions::from_mode(mode)).unwrap();
|
||||
let mut locked = false;
|
||||
|
||||
let result = download(
|
||||
&release(&base, hex_of(b"other")),
|
||||
dir.path(),
|
||||
&AtomicBool::new(false),
|
||||
IDLE_TIMEOUT,
|
||||
&mut |_, _| {
|
||||
if !locked {
|
||||
lock(0o555);
|
||||
locked = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let still_writable = fs::File::create(dir.path().join("probe")).is_ok();
|
||||
lock(0o755);
|
||||
if still_writable {
|
||||
println!("SKIP: permissions do not bind this user");
|
||||
return;
|
||||
}
|
||||
assert!(error_text(result).contains("the release says"));
|
||||
}
|
||||
|
||||
fn closed_within(flag: &AtomicBool, limit: Duration) -> bool {
|
||||
let started = Instant::now();
|
||||
while started.elapsed() < limit {
|
||||
if flag.load(Ordering::Relaxed) {
|
||||
return true;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_stalled_download_fails_and_frees_its_socket() {
|
||||
let closed = Arc::new(AtomicBool::new(false));
|
||||
let base = serve({
|
||||
let closed = closed.clone();
|
||||
move |_| vec![("/ElyDoc.bin".into(), Reply::Stall(closed))]
|
||||
});
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let stalled = download(
|
||||
&release(&base, hex_of(b"x")),
|
||||
dir.path(),
|
||||
&AtomicBool::new(false),
|
||||
Duration::from_millis(600),
|
||||
&mut |_, _| {},
|
||||
);
|
||||
|
||||
let text = error_text(stalled);
|
||||
assert!(text.contains("stalled"), "{text}");
|
||||
assert!(
|
||||
closed_within(&closed, Duration::from_secs(5)),
|
||||
"the client kept the socket open"
|
||||
);
|
||||
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cancel_is_answered_at_once_and_the_socket_is_freed_within_the_idle_time() {
|
||||
let closed = Arc::new(AtomicBool::new(false));
|
||||
let base = serve({
|
||||
let closed = closed.clone();
|
||||
move |_| vec![("/ElyDoc.bin".into(), Reply::Stall(closed))]
|
||||
});
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let cancel = AtomicBool::new(false);
|
||||
|
||||
let started = Instant::now();
|
||||
let outcome = thread::scope(|scope| {
|
||||
scope.spawn(|| {
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
cancel.store(true, Ordering::Relaxed);
|
||||
});
|
||||
download(
|
||||
&release(&base, hex_of(b"x")),
|
||||
dir.path(),
|
||||
&cancel,
|
||||
Duration::from_secs(1),
|
||||
&mut |_, _| {},
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(outcome.unwrap(), None);
|
||||
assert!(
|
||||
started.elapsed() < Duration::from_secs(1),
|
||||
"the cancel waited for the socket"
|
||||
);
|
||||
assert!(
|
||||
closed_within(&closed, Duration::from_secs(5)),
|
||||
"the client kept the socket open"
|
||||
);
|
||||
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_cancelled_download_returns_none_and_leaves_no_file_behind() {
|
||||
let body = payload();
|
||||
let base = serve(|_| vec![("/ElyDoc.bin".into(), Reply::Body(body.clone()))]);
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let cancel = AtomicBool::new(false);
|
||||
|
||||
let outcome = download(
|
||||
&release(&base, hex_of(&body)),
|
||||
dir.path(),
|
||||
&cancel,
|
||||
IDLE_TIMEOUT,
|
||||
&mut |_, _| {
|
||||
cancel.store(true, Ordering::Relaxed);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(outcome, None);
|
||||
assert_eq!(fs::read_dir(dir.path()).unwrap().count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_the_user_brought_is_checked_against_its_published_hash() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let file = dir.path().join("ElyDoc.dmg");
|
||||
fs::write(&file, b"installer bytes").unwrap();
|
||||
|
||||
verify_file(&file, &hex_of(b"installer bytes").to_uppercase()).unwrap();
|
||||
assert!(
|
||||
error_text(verify_file(&file, &hex_of(b"tampered bytes"))).contains("the release says")
|
||||
);
|
||||
assert!(error_text(verify_file(&dir.path().join("none"), "00")).contains("cannot open"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_and_downloads_are_logged_with_their_outcome() {
|
||||
let logs = tempfile::tempdir().unwrap();
|
||||
logging::init_in(AppId::Doc, logs.path()).unwrap();
|
||||
let body = payload();
|
||||
let sha = hex_of(&body);
|
||||
let base = serve(|base| {
|
||||
vec![
|
||||
(
|
||||
"/latest.json".into(),
|
||||
manifest("0.2.0", &platform(), base, &sha),
|
||||
),
|
||||
("/ElyDoc.bin".into(), Reply::Body(body.clone())),
|
||||
]
|
||||
});
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let cancel = AtomicBool::new(false);
|
||||
|
||||
check(&Version::new(0, 1, 0), &format!("{base}/latest.json")).unwrap();
|
||||
check(&Version::new(0, 1, 0), &format!("{base}/none.json")).unwrap_err();
|
||||
download(
|
||||
&release(&base, sha),
|
||||
dir.path(),
|
||||
&cancel,
|
||||
IDLE_TIMEOUT,
|
||||
&mut |_, _| {
|
||||
cancel.store(true, Ordering::Relaxed);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
log::logger().flush();
|
||||
|
||||
let text = fs::read_to_string(logs.path().join("ely.log")).unwrap();
|
||||
for line in [
|
||||
"update check: 127.0.0.1 offers 0.2.0",
|
||||
"update check: 127.0.0.1 failed",
|
||||
"update download: 0.2.0 cancelled",
|
||||
] {
|
||||
assert!(text.contains(line), "no '{line}' in:\n{text}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user