mirror of
https://github.com/bybrooklyn/alchemist.git
synced 2026-04-18 09:53:33 -04:00
chore: release v0.3.0-rc.3
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Lint
|
||||
run: cargo clippy --locked --all-targets --all-features -- -D warnings
|
||||
run: cargo clippy --locked --all-targets --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used
|
||||
|
||||
- name: Check
|
||||
run: cargo check --locked --all-targets --all-features
|
||||
|
||||
2
.github/workflows/nightly.yml
vendored
2
.github/workflows/nightly.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Lint
|
||||
run: cargo clippy --locked --all-targets --all-features -- -D warnings
|
||||
run: cargo clippy --locked --all-targets --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used
|
||||
|
||||
- name: Check
|
||||
run: cargo check --locked --all-targets --all-features
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.3.0-rc.2] - 2026-04-04
|
||||
## [0.3.0-rc.3] - 2026-04-04
|
||||
|
||||
### Release Engineering
|
||||
- Added Windows-specific contributor scripts for the core `just install-w`, `just dev`, and `just check` path, while keeping the broader Unix-oriented utility and release recipes unchanged for now.
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -13,7 +13,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "alchemist"
|
||||
version = "0.3.0-rc.2"
|
||||
version = "0.3.0-rc.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "alchemist"
|
||||
version = "0.3.0-rc.2"
|
||||
version = "0.3.0-rc.3"
|
||||
edition = "2024"
|
||||
rust-version = "1.85"
|
||||
license = "GPL-3.0"
|
||||
|
||||
@@ -3,7 +3,7 @@ title: Changelog
|
||||
description: Release history for Alchemist.
|
||||
---
|
||||
|
||||
## [0.3.0-rc.2] - 2026-04-04
|
||||
## [0.3.0-rc.3] - 2026-04-04
|
||||
|
||||
### Release Engineering
|
||||
- Added Windows-specific contributor scripts for the core `just install-w`, `just dev`, and `just check` path, while leaving broader utility and release recipes Unix-first for now.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "alchemist-docs",
|
||||
"version": "0.3.0-rc.2",
|
||||
"version": "0.3.0-rc.3",
|
||||
"private": true,
|
||||
"packageManager": "bun@1.3.5",
|
||||
"scripts": {
|
||||
|
||||
6
justfile
6
justfile
@@ -109,7 +109,7 @@ check-u:
|
||||
@echo "── Rust format ──"
|
||||
cargo fmt --all -- --check
|
||||
@echo "── Rust clippy ──"
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
cargo clippy --all-targets --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used
|
||||
@echo "── Rust check ──"
|
||||
cargo check --all-targets
|
||||
@echo "── Frontend typecheck ──"
|
||||
@@ -122,7 +122,7 @@ check-w:
|
||||
# Rust checks only (faster)
|
||||
check-rust:
|
||||
cargo fmt --all -- --check
|
||||
cargo clippy --all-targets --all-features -- -D warnings
|
||||
cargo clippy --all-targets --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used
|
||||
cargo check --all-targets
|
||||
|
||||
# Frontend checks only
|
||||
@@ -234,7 +234,7 @@ release-verify:
|
||||
@echo "── Rust format ──"
|
||||
cargo fmt --all -- --check
|
||||
@echo "── Rust clippy ──"
|
||||
cargo clippy --locked --all-targets --all-features -- -D warnings
|
||||
cargo clippy --locked --all-targets --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used
|
||||
@echo "── Rust check ──"
|
||||
cargo check --locked --all-targets --all-features
|
||||
@echo "── Rust tests ──"
|
||||
|
||||
@@ -830,7 +830,10 @@ mod tests {
|
||||
notify_on_failure = true
|
||||
"#;
|
||||
|
||||
let mut config: Config = toml::from_str(raw).expect("config");
|
||||
let mut config: Config = match toml::from_str(raw) {
|
||||
Ok(config) => config,
|
||||
Err(err) => panic!("failed to parse config fixture: {err}"),
|
||||
};
|
||||
config.migrate_legacy_notifications();
|
||||
|
||||
assert_eq!(config.notifications.targets.len(), 1);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![deny(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
pub mod config;
|
||||
pub mod db;
|
||||
pub mod error;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![deny(clippy::expect_used, clippy::unwrap_used)]
|
||||
|
||||
use alchemist::db::EventChannels;
|
||||
use alchemist::error::Result;
|
||||
use alchemist::system::hardware;
|
||||
@@ -979,8 +981,8 @@ mod tests {
|
||||
|
||||
assert_eq!(detected.vendor, hardware::Vendor::Cpu);
|
||||
assert_eq!(
|
||||
hardware_state.snapshot().await.unwrap().vendor,
|
||||
hardware::Vendor::Cpu
|
||||
hardware_state.snapshot().await.map(|info| info.vendor),
|
||||
Some(hardware::Vendor::Cpu)
|
||||
);
|
||||
|
||||
let config_guard = config_state.read().await;
|
||||
|
||||
@@ -369,7 +369,9 @@ mod tests {
|
||||
SystemTime::UNIX_EPOCH,
|
||||
)
|
||||
.await?;
|
||||
let job = db.get_job_by_input_path("input.mkv").await?.expect("job");
|
||||
let Some(job) = db.get_job_by_input_path("input.mkv").await? else {
|
||||
panic!("expected seeded job");
|
||||
};
|
||||
let (tx, mut rx) = broadcast::channel(8);
|
||||
let (jobs_tx, _) = broadcast::channel(100);
|
||||
let (config_tx, _) = broadcast::channel(10);
|
||||
@@ -396,7 +398,9 @@ mod tests {
|
||||
let logs = db.get_logs(10, 0).await?;
|
||||
assert_eq!(logs[0].message, "ffmpeg line");
|
||||
|
||||
let updated = db.get_job(job.id).await?.expect("updated");
|
||||
let Some(updated) = db.get_job(job.id).await? else {
|
||||
panic!("expected updated job");
|
||||
};
|
||||
assert!((updated.progress - 20.0).abs() < 0.01);
|
||||
|
||||
let first = rx.recv().await?;
|
||||
|
||||
@@ -891,7 +891,8 @@ mod tests {
|
||||
fn test_progress_parsing() {
|
||||
let line =
|
||||
"frame= 100 fps=25.0 bitrate=1500kbps total_size=1000000 time=00:00:04.00 speed=1.5x";
|
||||
let progress = FFmpegProgress::parse_line(line).expect("expected progress parse");
|
||||
let progress =
|
||||
FFmpegProgress::parse_line(line).unwrap_or_else(|| panic!("expected progress parse"));
|
||||
|
||||
assert_eq!(progress.frame, 100);
|
||||
assert_eq!(progress.fps, 25.0);
|
||||
@@ -906,7 +907,7 @@ mod tests {
|
||||
assert!(state.ingest_line("out_time=00:00:01.50").is_none());
|
||||
let progress = state
|
||||
.ingest_line("progress=continue")
|
||||
.expect("expected structured progress");
|
||||
.unwrap_or_else(|| panic!("expected structured progress"));
|
||||
assert_eq!(progress.frame, 42);
|
||||
assert!((progress.time_seconds - 1.5).abs() < 0.01);
|
||||
}
|
||||
@@ -921,7 +922,9 @@ mod tests {
|
||||
&metadata,
|
||||
&plan,
|
||||
);
|
||||
let args = builder.build_args().expect("args");
|
||||
let args = builder
|
||||
.build_args()
|
||||
.unwrap_or_else(|err| panic!("failed to build cpu args: {err}"));
|
||||
assert!(args.contains(&"libx264".to_string()));
|
||||
assert!(args.contains(&"-progress".to_string()));
|
||||
}
|
||||
@@ -936,7 +939,9 @@ mod tests {
|
||||
&metadata,
|
||||
&plan,
|
||||
);
|
||||
let args = builder.build_args().expect("args");
|
||||
let args = builder
|
||||
.build_args()
|
||||
.unwrap_or_else(|err| panic!("failed to build nvenc args: {err}"));
|
||||
assert!(args.contains(&"hevc_nvenc".to_string()));
|
||||
assert!(args.contains(&"p4".to_string()));
|
||||
}
|
||||
@@ -953,7 +958,9 @@ mod tests {
|
||||
&plan,
|
||||
)
|
||||
.with_hardware(Some(&hw_info));
|
||||
let args = builder.build_args().expect("args");
|
||||
let args = builder
|
||||
.build_args()
|
||||
.unwrap_or_else(|err| panic!("failed to build qsv args: {err}"));
|
||||
assert!(args.contains(&"av1_qsv".to_string()));
|
||||
assert!(args.contains(&"-init_hw_device".to_string()));
|
||||
}
|
||||
@@ -977,7 +984,9 @@ mod tests {
|
||||
&plan,
|
||||
)
|
||||
.with_hardware(Some(&info));
|
||||
let args = builder.build_args().expect("args");
|
||||
let args = builder
|
||||
.build_args()
|
||||
.unwrap_or_else(|err| panic!("failed to build vaapi args: {err}"));
|
||||
assert!(args.contains(&"hevc_vaapi".to_string()));
|
||||
assert!(args.iter().any(|arg| arg.contains("format=nv12,hwupload")));
|
||||
}
|
||||
@@ -992,7 +1001,9 @@ mod tests {
|
||||
&metadata,
|
||||
&plan,
|
||||
);
|
||||
let args = builder.build_args().expect("args");
|
||||
let args = builder
|
||||
.build_args()
|
||||
.unwrap_or_else(|err| panic!("failed to build videotoolbox args: {err}"));
|
||||
assert!(args.contains(&"hevc_videotoolbox".to_string()));
|
||||
}
|
||||
|
||||
@@ -1006,7 +1017,9 @@ mod tests {
|
||||
&metadata,
|
||||
&plan,
|
||||
);
|
||||
let args = builder.build_args().expect("args");
|
||||
let args = builder
|
||||
.build_args()
|
||||
.unwrap_or_else(|err| panic!("failed to build amf args: {err}"));
|
||||
assert!(args.contains(&"hevc_amf".to_string()));
|
||||
}
|
||||
|
||||
@@ -1027,7 +1040,9 @@ mod tests {
|
||||
&metadata,
|
||||
&plan,
|
||||
);
|
||||
let args = builder.build_args().expect("args");
|
||||
let args = builder
|
||||
.build_args()
|
||||
.unwrap_or_else(|err| panic!("failed to build mp4 args: {err}"));
|
||||
assert!(args.contains(&"aac".to_string()));
|
||||
assert!(args.contains(&"aac_low".to_string()));
|
||||
assert!(args.contains(&"+faststart".to_string()));
|
||||
@@ -1081,8 +1096,8 @@ mod tests {
|
||||
);
|
||||
let args = builder
|
||||
.build_subtitle_extract_args()
|
||||
.expect("args")
|
||||
.expect("subtitle extract args");
|
||||
.unwrap_or_else(|err| panic!("failed to build subtitle extract args: {err}"))
|
||||
.unwrap_or_else(|| panic!("expected subtitle extract args"));
|
||||
assert!(args.contains(&"0:s:0".to_string()));
|
||||
assert!(args.contains(&"0:s:1".to_string()));
|
||||
assert!(args.contains(&"srt".to_string()));
|
||||
@@ -1106,7 +1121,9 @@ mod tests {
|
||||
&metadata,
|
||||
&plan,
|
||||
);
|
||||
let args = builder.build_args().expect("args");
|
||||
let args = builder
|
||||
.build_args()
|
||||
.unwrap_or_else(|err| panic!("failed to build remux args: {err}"));
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
@@ -1135,7 +1152,9 @@ mod tests {
|
||||
&metadata,
|
||||
&plan,
|
||||
);
|
||||
let args = builder.build_args().expect("args");
|
||||
let args = builder
|
||||
.build_args()
|
||||
.unwrap_or_else(|err| panic!("failed to build selected audio args: {err}"));
|
||||
assert!(args.contains(&"0:a:0".to_string()));
|
||||
assert!(args.contains(&"0:a:2".to_string()));
|
||||
assert!(!args.contains(&"0:a?".to_string()));
|
||||
@@ -1147,7 +1166,8 @@ mod tests {
|
||||
stdout: b"Encoders:\n V..... libx264 H.264\n A..... aac AAC\n V..... av1_qsv AV1\n"
|
||||
.to_vec(),
|
||||
};
|
||||
let capabilities = EncoderCapabilities::detect_with_runner(&runner).expect("capabilities");
|
||||
let capabilities = EncoderCapabilities::detect_with_runner(&runner)
|
||||
.unwrap_or_else(|err| panic!("failed to detect capabilities: {err}"));
|
||||
assert!(capabilities.has_video_encoder("libx264"));
|
||||
assert!(capabilities.has_video_encoder("av1_qsv"));
|
||||
assert!(capabilities.audio_encoders.contains("aac"));
|
||||
@@ -1158,7 +1178,8 @@ mod tests {
|
||||
let runner = FakeRunner {
|
||||
stdout: b"Hardware acceleration methods:\nvaapi\nqsv\n".to_vec(),
|
||||
};
|
||||
let accelerators = HardwareAccelerators::detect_with_runner(&runner).expect("hwaccels");
|
||||
let accelerators = HardwareAccelerators::detect_with_runner(&runner)
|
||||
.unwrap_or_else(|err| panic!("failed to detect accelerators: {err}"));
|
||||
assert!(accelerators.available.contains("vaapi"));
|
||||
assert!(accelerators.available.contains("qsv"));
|
||||
}
|
||||
@@ -1166,7 +1187,8 @@ mod tests {
|
||||
#[test]
|
||||
fn test_vmaf_score_text_parse() {
|
||||
let stderr = "Some log\nVMAF score: 93.2\nMore log";
|
||||
let vmaf = QualityScore::extract_vmaf_score_text(stderr).expect("expected vmaf");
|
||||
let vmaf = QualityScore::extract_vmaf_score_text(stderr)
|
||||
.unwrap_or_else(|| panic!("expected vmaf score from text output"));
|
||||
assert!((vmaf - 93.2).abs() < 0.01);
|
||||
}
|
||||
|
||||
@@ -1180,7 +1202,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
let vmaf = QualityScore::extract_vmaf_score_json(json).expect("expected vmaf");
|
||||
let vmaf = QualityScore::extract_vmaf_score_json(json)
|
||||
.unwrap_or_else(|| panic!("expected vmaf score from json output"));
|
||||
assert!((vmaf - 87.65).abs() < 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1179,7 +1179,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mp4_subtitle_copy_fails_fast() {
|
||||
let reason = plan_subtitles(
|
||||
let reason = match plan_subtitles(
|
||||
&[SubtitleStreamMetadata {
|
||||
stream_index: 0,
|
||||
codec_name: "subrip".to_string(),
|
||||
@@ -1192,8 +1192,10 @@ mod tests {
|
||||
"mp4",
|
||||
Path::new("/tmp/out.mp4"),
|
||||
SubtitleMode::Copy,
|
||||
)
|
||||
.unwrap_err();
|
||||
) {
|
||||
Ok(_) => panic!("expected mp4 subtitle copy planning to fail"),
|
||||
Err(reason) => reason,
|
||||
};
|
||||
assert!(reason.contains("cannot safely copy"));
|
||||
}
|
||||
|
||||
@@ -1279,13 +1281,13 @@ mod tests {
|
||||
Path::new("/tmp/out.mkv"),
|
||||
SubtitleMode::Burn,
|
||||
)
|
||||
.expect("burn plan");
|
||||
.unwrap_or_else(|err| panic!("failed to build burn plan: {err}"));
|
||||
assert!(matches!(plan, SubtitleStreamPlan::Burn { stream_index: 2 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn burn_fails_without_burnable_text_stream() {
|
||||
let reason = plan_subtitles(
|
||||
let reason = match plan_subtitles(
|
||||
&[SubtitleStreamMetadata {
|
||||
stream_index: 0,
|
||||
codec_name: "hdmv_pgs_subtitle".to_string(),
|
||||
@@ -1298,8 +1300,10 @@ mod tests {
|
||||
"mkv",
|
||||
Path::new("/tmp/out.mkv"),
|
||||
SubtitleMode::Burn,
|
||||
)
|
||||
.unwrap_err();
|
||||
) {
|
||||
Ok(_) => panic!("expected burn planning to fail without a burnable stream"),
|
||||
Err(reason) => reason,
|
||||
};
|
||||
assert!(reason.contains("No burnable"));
|
||||
}
|
||||
|
||||
@@ -1319,7 +1323,7 @@ mod tests {
|
||||
Path::new("/tmp/library/movie-alchemist.mkv"),
|
||||
SubtitleMode::Extract,
|
||||
)
|
||||
.expect("extract plan");
|
||||
.unwrap_or_else(|err| panic!("failed to build extract plan: {err}"));
|
||||
|
||||
match plan {
|
||||
SubtitleStreamPlan::Extract { outputs } => {
|
||||
@@ -1366,7 +1370,7 @@ mod tests {
|
||||
Path::new("/tmp/library/movie-alchemist.mkv"),
|
||||
SubtitleMode::Extract,
|
||||
)
|
||||
.expect("extract plan");
|
||||
.unwrap_or_else(|err| panic!("failed to build extract plan: {err}"));
|
||||
|
||||
match plan {
|
||||
SubtitleStreamPlan::Extract { outputs } => {
|
||||
@@ -1492,7 +1496,7 @@ mod tests {
|
||||
let plan = planner
|
||||
.plan(&analysis(), Path::new("/tmp/out.mkv"), None)
|
||||
.await
|
||||
.expect("plan");
|
||||
.unwrap_or_else(|err| panic!("failed to build no-encoder plan: {err}"));
|
||||
|
||||
let TranscodeDecision::Skip { reason } = plan.decision else {
|
||||
panic!("expected skip decision");
|
||||
@@ -1528,7 +1532,7 @@ mod tests {
|
||||
let plan = planner
|
||||
.plan(&analysis(), Path::new("/tmp/out.mkv"), None)
|
||||
.await
|
||||
.expect("plan");
|
||||
.unwrap_or_else(|err| panic!("failed to build preferred-codec plan: {err}"));
|
||||
|
||||
let TranscodeDecision::Skip { reason } = plan.decision else {
|
||||
panic!("expected skip decision");
|
||||
@@ -1547,10 +1551,13 @@ mod tests {
|
||||
cpu: vec![Encoder::Av1Svt],
|
||||
};
|
||||
|
||||
let (encoder, fallback) =
|
||||
select_encoder(OutputCodec::Av1, &inventory, true).expect("selected encoder");
|
||||
let (encoder, fallback) = select_encoder(OutputCodec::Av1, &inventory, true)
|
||||
.unwrap_or_else(|| panic!("expected selected encoder"));
|
||||
assert_eq!(encoder, Encoder::HevcQsv);
|
||||
assert_eq!(fallback.expect("fallback").kind, FallbackKind::Codec);
|
||||
assert_eq!(
|
||||
fallback.unwrap_or_else(|| panic!("expected fallback")).kind,
|
||||
FallbackKind::Codec
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1560,8 +1567,8 @@ mod tests {
|
||||
cpu: Vec::new(),
|
||||
};
|
||||
|
||||
let (encoder, fallback) =
|
||||
select_encoder(OutputCodec::Av1, &inventory, false).expect("selected encoder");
|
||||
let (encoder, fallback) = select_encoder(OutputCodec::Av1, &inventory, false)
|
||||
.unwrap_or_else(|| panic!("expected selected encoder"));
|
||||
assert_eq!(encoder, Encoder::Av1Vaapi);
|
||||
assert!(fallback.is_none());
|
||||
}
|
||||
@@ -1583,8 +1590,8 @@ mod tests {
|
||||
cpu: vec![Encoder::Av1Svt],
|
||||
};
|
||||
|
||||
let (encoder, fallback) =
|
||||
select_encoder(OutputCodec::Av1, &inventory, false).expect("selected encoder");
|
||||
let (encoder, fallback) = select_encoder(OutputCodec::Av1, &inventory, false)
|
||||
.unwrap_or_else(|| panic!("expected selected encoder"));
|
||||
assert_eq!(encoder, Encoder::Av1Svt);
|
||||
assert!(fallback.is_none());
|
||||
}
|
||||
|
||||
@@ -110,8 +110,7 @@ mod tests {
|
||||
#[test]
|
||||
fn resolve_source_root_prefers_longest_matching_root() {
|
||||
let roots = vec![PathBuf::from("/media"), PathBuf::from("/media/movies")];
|
||||
let resolved =
|
||||
resolve_source_root(Path::new("/media/movies/action/example.mkv"), &roots).unwrap();
|
||||
assert_eq!(resolved, PathBuf::from("/media/movies"));
|
||||
let resolved = resolve_source_root(Path::new("/media/movies/action/example.mkv"), &roots);
|
||||
assert_eq!(resolved, Some(PathBuf::from("/media/movies")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,11 +511,14 @@ mod tests {
|
||||
let addr = listener.local_addr()?;
|
||||
|
||||
let body_task = tokio::spawn(async move {
|
||||
let (mut socket, _) = listener.accept().await.expect("accept");
|
||||
let (mut socket, _) = match listener.accept().await {
|
||||
Ok(socket) => socket,
|
||||
Err(err) => return Err::<String, std::io::Error>(err),
|
||||
};
|
||||
let mut buf = Vec::new();
|
||||
let mut chunk = [0u8; 4096];
|
||||
loop {
|
||||
let read = socket.read(&mut chunk).await.expect("read");
|
||||
let read = socket.read(&mut chunk).await?;
|
||||
if read == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -525,8 +528,8 @@ mod tests {
|
||||
}
|
||||
}
|
||||
let response = "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n";
|
||||
socket.write_all(response.as_bytes()).await.expect("write");
|
||||
String::from_utf8_lossy(&buf).to_string()
|
||||
socket.write_all(response.as_bytes()).await?;
|
||||
Ok(String::from_utf8_lossy(&buf).to_string())
|
||||
});
|
||||
|
||||
let target = NotificationTarget {
|
||||
@@ -545,7 +548,7 @@ mod tests {
|
||||
};
|
||||
|
||||
manager.send(&target, &event, "failed").await?;
|
||||
let request = body_task.await?;
|
||||
let request = body_task.await??;
|
||||
let body = request
|
||||
.split("\r\n\r\n")
|
||||
.nth(1)
|
||||
|
||||
@@ -124,9 +124,9 @@ mod tests {
|
||||
fn default_dir_falls_back_to_home_config() {
|
||||
// SAFETY: single-threaded test.
|
||||
unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
|
||||
// HOME is always set in a test environment
|
||||
let home = std::env::var("HOME").unwrap();
|
||||
let expected = PathBuf::from(&home).join(".config").join("alchemist");
|
||||
assert_eq!(default_data_dir(), expected);
|
||||
let expected = std::env::var_os("HOME")
|
||||
.map(PathBuf::from)
|
||||
.map(|home| home.join(".config").join("alchemist"));
|
||||
assert_eq!(Some(default_data_dir()), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,15 +40,25 @@ pub(crate) async fn login_handler(
|
||||
|
||||
// A valid argon2 static hash of a random string used to simulate work and equalize timing
|
||||
const DUMMY_HASH: &str = "$argon2id$v=19$m=19456,t=2,p=1$c2FsdHN0cmluZzEyMzQ1Ng$1tJ2tA109qj15m3u5+kS/sX5X1UoZ6/H9b/30tX9N/g";
|
||||
let dummy_hash = match PasswordHash::new(DUMMY_HASH) {
|
||||
Ok(hash) => hash,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
"Authentication fallback hash is invalid",
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let parsed_hash = match &user_result {
|
||||
Some(u) => PasswordHash::new(&u.password_hash).unwrap_or_else(|_| {
|
||||
is_valid = false;
|
||||
PasswordHash::new(DUMMY_HASH).expect("DUMMY_HASH must be a valid argon2 hash")
|
||||
dummy_hash.clone()
|
||||
}),
|
||||
None => {
|
||||
is_valid = false;
|
||||
PasswordHash::new(DUMMY_HASH).expect("DUMMY_HASH must be a valid argon2 hash")
|
||||
dummy_hash
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use super::AppState;
|
||||
use axum::{
|
||||
extract::{ConnectInfo, Request, State},
|
||||
http::{StatusCode, header},
|
||||
http::{HeaderName, HeaderValue, StatusCode, header},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
@@ -29,50 +29,39 @@ pub(crate) async fn security_headers_middleware(request: Request, next: Next) ->
|
||||
let headers = response.headers_mut();
|
||||
|
||||
// Prevent clickjacking
|
||||
headers.insert(
|
||||
header::X_FRAME_OPTIONS,
|
||||
"DENY".parse().expect("valid header value"),
|
||||
);
|
||||
headers.insert(header::X_FRAME_OPTIONS, HeaderValue::from_static("DENY"));
|
||||
|
||||
// Prevent MIME type sniffing
|
||||
headers.insert(
|
||||
header::X_CONTENT_TYPE_OPTIONS,
|
||||
"nosniff".parse().expect("valid header value"),
|
||||
HeaderValue::from_static("nosniff"),
|
||||
);
|
||||
|
||||
// XSS protection (legacy but still useful)
|
||||
headers.insert(
|
||||
"X-XSS-Protection"
|
||||
.parse::<axum::http::HeaderName>()
|
||||
.expect("valid header name"),
|
||||
"1; mode=block".parse().expect("valid header value"),
|
||||
HeaderName::from_static("x-xss-protection"),
|
||||
HeaderValue::from_static("1; mode=block"),
|
||||
);
|
||||
|
||||
// Content Security Policy - allows inline scripts/styles for the SPA
|
||||
// This is permissive enough for the app while still providing protection
|
||||
headers.insert(
|
||||
header::CONTENT_SECURITY_POLICY,
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; frame-ancestors 'none'"
|
||||
.parse()
|
||||
.expect("valid CSP header value"),
|
||||
HeaderValue::from_static(
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; frame-ancestors 'none'",
|
||||
),
|
||||
);
|
||||
|
||||
// Referrer policy
|
||||
headers.insert(
|
||||
header::REFERRER_POLICY,
|
||||
"strict-origin-when-cross-origin"
|
||||
.parse()
|
||||
.expect("valid header value"),
|
||||
HeaderValue::from_static("strict-origin-when-cross-origin"),
|
||||
);
|
||||
|
||||
// Permissions policy (restrict browser features)
|
||||
headers.insert(
|
||||
"Permissions-Policy"
|
||||
.parse::<axum::http::HeaderName>()
|
||||
.expect("valid header name"),
|
||||
"geolocation=(), microphone=(), camera=()"
|
||||
.parse()
|
||||
.expect("valid header value"),
|
||||
HeaderName::from_static("permissions-policy"),
|
||||
HeaderValue::from_static("geolocation=(), microphone=(), camera=()"),
|
||||
);
|
||||
|
||||
response
|
||||
|
||||
@@ -242,17 +242,23 @@ pub async fn run_server(args: RunServerArgs) -> Result<()> {
|
||||
.with_graceful_shutdown(async move {
|
||||
// Wait for shutdown signal
|
||||
let ctrl_c = async {
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.expect("failed to install Ctrl+C handler");
|
||||
if let Err(err) = tokio::signal::ctrl_c().await {
|
||||
error!("Failed to install Ctrl+C handler: {}", err);
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
let terminate = async {
|
||||
tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
|
||||
.expect("failed to install signal handler")
|
||||
.recv()
|
||||
.await;
|
||||
match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) {
|
||||
Ok(mut signal) => {
|
||||
signal.recv().await;
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to install signal handler: {}", err);
|
||||
std::future::pending::<()>().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(unix))]
|
||||
|
||||
@@ -136,12 +136,15 @@ async fn create_session(
|
||||
}
|
||||
|
||||
fn auth_request(method: Method, uri: &str, token: &str, body: Body) -> Request<Body> {
|
||||
Request::builder()
|
||||
match Request::builder()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.header(header::COOKIE, format!("alchemist_session={token}"))
|
||||
.body(body)
|
||||
.unwrap()
|
||||
{
|
||||
Ok(request) => request,
|
||||
Err(err) => panic!("failed to build auth request: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn auth_json_request(
|
||||
@@ -150,21 +153,23 @@ fn auth_json_request(
|
||||
token: &str,
|
||||
body: serde_json::Value,
|
||||
) -> Request<Body> {
|
||||
Request::builder()
|
||||
match Request::builder()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.header(header::COOKIE, format!("alchemist_session={token}"))
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from(body.to_string()))
|
||||
.unwrap()
|
||||
{
|
||||
Ok(request) => request,
|
||||
Err(err) => panic!("failed to build auth json request: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn localhost_request(method: Method, uri: &str, body: Body) -> Request<Body> {
|
||||
let mut request = Request::builder()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.body(body)
|
||||
.unwrap();
|
||||
let mut request = match Request::builder().method(method).uri(uri).body(body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => panic!("failed to build localhost request: {err}"),
|
||||
};
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(ConnectInfo(SocketAddr::from(([127, 0, 0, 1], 3000))));
|
||||
@@ -172,11 +177,10 @@ fn localhost_request(method: Method, uri: &str, body: Body) -> Request<Body> {
|
||||
}
|
||||
|
||||
fn remote_request(method: Method, uri: &str, body: Body) -> Request<Body> {
|
||||
let mut request = Request::builder()
|
||||
.method(method)
|
||||
.uri(uri)
|
||||
.body(body)
|
||||
.unwrap();
|
||||
let mut request = match Request::builder().method(method).uri(uri).body(body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => panic!("failed to build remote request: {err}"),
|
||||
};
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(ConnectInfo(SocketAddr::from(([203, 0, 113, 10], 3000))));
|
||||
@@ -184,8 +188,14 @@ fn remote_request(method: Method, uri: &str, body: Body) -> Request<Body> {
|
||||
}
|
||||
|
||||
async fn body_text(response: axum::response::Response) -> String {
|
||||
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
String::from_utf8(bytes.to_vec()).unwrap()
|
||||
let bytes = match to_bytes(response.into_body(), usize::MAX).await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(err) => panic!("failed to read response body: {err}"),
|
||||
};
|
||||
match String::from_utf8(bytes.to_vec()) {
|
||||
Ok(body) => body,
|
||||
Err(err) => panic!("response body was not utf-8: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn seed_job(
|
||||
@@ -198,15 +208,19 @@ async fn seed_job(
|
||||
|
||||
db.enqueue_job(&input, &output, std::time::SystemTime::UNIX_EPOCH)
|
||||
.await?;
|
||||
let job = db
|
||||
let Some(job) = db
|
||||
.get_job_by_input_path(input.to_string_lossy().as_ref())
|
||||
.await?
|
||||
.expect("job");
|
||||
else {
|
||||
panic!("expected seeded job");
|
||||
};
|
||||
if job.status != status {
|
||||
db.update_job_status(job.id, status).await?;
|
||||
}
|
||||
|
||||
let job = db.get_job_by_id(job.id).await?.expect("job by id");
|
||||
let Some(job) = db.get_job_by_id(job.id).await? else {
|
||||
panic!("expected seeded job by id");
|
||||
};
|
||||
Ok((job, input, output))
|
||||
}
|
||||
|
||||
@@ -259,8 +273,8 @@ fn validate_transcode_payload_rejects_invalid_values() {
|
||||
fn normalize_setup_directories_trims_and_filters() {
|
||||
let movies_dir = temp_path("alchemist_setup_movies", "dir");
|
||||
let tv_dir = temp_path("alchemist_setup_tv", "dir");
|
||||
std::fs::create_dir_all(&movies_dir).unwrap();
|
||||
std::fs::create_dir_all(&tv_dir).unwrap();
|
||||
assert!(std::fs::create_dir_all(&movies_dir).is_ok());
|
||||
assert!(std::fs::create_dir_all(&tv_dir).is_ok());
|
||||
|
||||
let input = vec![
|
||||
format!(" {} ", movies_dir.to_string_lossy()),
|
||||
@@ -269,16 +283,19 @@ fn normalize_setup_directories_trims_and_filters() {
|
||||
tv_dir.to_string_lossy().to_string(),
|
||||
];
|
||||
|
||||
let normalized = normalize_setup_directories(&input).expect("normalize");
|
||||
let normalized = match normalize_setup_directories(&input) {
|
||||
Ok(normalized) => normalized,
|
||||
Err(err) => panic!("failed to normalize setup directories: {err}"),
|
||||
};
|
||||
assert_eq!(
|
||||
normalized,
|
||||
vec![
|
||||
std::fs::canonicalize(&movies_dir)
|
||||
.unwrap()
|
||||
.unwrap_or_else(|err| panic!("failed to canonicalize movies dir: {err}"))
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
std::fs::canonicalize(&tv_dir)
|
||||
.unwrap()
|
||||
.unwrap_or_else(|err| panic!("failed to canonicalize tv dir: {err}"))
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
]
|
||||
@@ -322,22 +339,28 @@ async fn sse_unified_stream_emits_lagged_event_and_recovers() {
|
||||
job_id: Some(1),
|
||||
message: "first".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
.unwrap_or_else(|err| panic!("failed to send first log event: {err}"));
|
||||
job_tx
|
||||
.send(JobEvent::Log {
|
||||
level: "info".to_string(),
|
||||
job_id: Some(1),
|
||||
message: "second".to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
.unwrap_or_else(|err| panic!("failed to send second log event: {err}"));
|
||||
drop(job_tx);
|
||||
|
||||
let mut stream = Box::pin(super::sse::sse_unified_stream(job_rx, config_rx, system_rx));
|
||||
let first = stream.next().await.unwrap().unwrap();
|
||||
let Some(first) = stream.next().await else {
|
||||
panic!("expected first SSE event");
|
||||
};
|
||||
let first = first.unwrap_or_else(|_| panic!("expected first SSE event payload"));
|
||||
assert_eq!(first.event_name, "lagged");
|
||||
assert!(first.data.contains("\"skipped\":1"));
|
||||
|
||||
let second = stream.next().await.unwrap().unwrap();
|
||||
let Some(second) = stream.next().await else {
|
||||
panic!("expected second SSE event");
|
||||
};
|
||||
let second = second.unwrap_or_else(|_| panic!("expected second SSE event payload"));
|
||||
assert_eq!(second.event_name, "log");
|
||||
assert!(second.data.contains("\"second\""));
|
||||
}
|
||||
@@ -365,8 +388,14 @@ async fn hardware_settings_route_updates_runtime_state()
|
||||
.await?;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let hardware = state.hardware_state.snapshot().await.unwrap();
|
||||
assert_eq!(hardware.vendor, crate::system::hardware::Vendor::Cpu);
|
||||
assert_eq!(
|
||||
state
|
||||
.hardware_state
|
||||
.snapshot()
|
||||
.await
|
||||
.map(|info| info.vendor),
|
||||
Some(crate::system::hardware::Vendor::Cpu)
|
||||
);
|
||||
|
||||
let response = app
|
||||
.clone()
|
||||
@@ -558,7 +587,7 @@ async fn setup_complete_updates_runtime_hardware_without_mirroring_watch_dirs()
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
.unwrap_or_else(|err| panic!("failed to build setup completion request: {err}")),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
@@ -567,11 +596,9 @@ async fn setup_complete_updates_runtime_hardware_without_mirroring_watch_dirs()
|
||||
.headers()
|
||||
.get(header::SET_COOKIE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.unwrap()
|
||||
.split(';')
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
.map(|value| value.split(';').next().unwrap_or("").to_string())
|
||||
.unwrap_or_default();
|
||||
assert!(!set_cookie.is_empty());
|
||||
|
||||
assert!(
|
||||
!state
|
||||
@@ -579,8 +606,12 @@ async fn setup_complete_updates_runtime_hardware_without_mirroring_watch_dirs()
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
);
|
||||
assert_eq!(
|
||||
state.hardware_state.snapshot().await.unwrap().vendor,
|
||||
crate::system::hardware::Vendor::Cpu
|
||||
state
|
||||
.hardware_state
|
||||
.snapshot()
|
||||
.await
|
||||
.map(|info| info.vendor),
|
||||
Some(crate::system::hardware::Vendor::Cpu)
|
||||
);
|
||||
|
||||
let watch_dirs = state.db.get_watch_dirs().await?;
|
||||
@@ -605,7 +636,7 @@ async fn setup_complete_updates_runtime_hardware_without_mirroring_watch_dirs()
|
||||
.uri("/api/system/hardware")
|
||||
.header(header::COOKIE, set_cookie)
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
.unwrap_or_else(|err| panic!("failed to build hardware request: {err}")),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
@@ -661,7 +692,9 @@ async fn setup_complete_accepts_nested_settings_payload()
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("failed to build nested setup completion request: {err}")
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
@@ -708,7 +741,9 @@ async fn setup_complete_rejects_nested_settings_without_library_directories()
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.unwrap(),
|
||||
.unwrap_or_else(|err| {
|
||||
panic!("failed to build nested setup rejection request: {err}")
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
@@ -755,9 +790,10 @@ async fn fs_endpoints_are_available_during_setup()
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
request
|
||||
.headers_mut()
|
||||
.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
||||
request.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
axum::http::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
request
|
||||
})
|
||||
.await?;
|
||||
@@ -799,9 +835,10 @@ async fn fs_endpoints_require_loopback_during_setup()
|
||||
.to_string(),
|
||||
),
|
||||
);
|
||||
preview_request
|
||||
.headers_mut()
|
||||
.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
|
||||
preview_request.headers_mut().insert(
|
||||
header::CONTENT_TYPE,
|
||||
axum::http::HeaderValue::from_static("application/json"),
|
||||
);
|
||||
|
||||
let preview_response = app.clone().oneshot(preview_request).await?;
|
||||
assert_eq!(preview_response.status(), StatusCode::FORBIDDEN);
|
||||
@@ -824,7 +861,7 @@ async fn settings_bundle_requires_auth_after_setup()
|
||||
.method(Method::GET)
|
||||
.uri("/api/settings/bundle")
|
||||
.body(Body::empty())
|
||||
.unwrap(),
|
||||
.unwrap_or_else(|err| panic!("failed to build settings bundle GET request: {err}")),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(get_response.status(), StatusCode::UNAUTHORIZED);
|
||||
@@ -837,7 +874,7 @@ async fn settings_bundle_requires_auth_after_setup()
|
||||
.uri("/api/settings/bundle")
|
||||
.header(header::CONTENT_TYPE, "application/json")
|
||||
.body(Body::from("{}"))
|
||||
.unwrap(),
|
||||
.unwrap_or_else(|err| panic!("failed to build settings bundle PUT request: {err}")),
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(put_response.status(), StatusCode::UNAUTHORIZED);
|
||||
@@ -1216,7 +1253,7 @@ async fn jobs_table_includes_structured_decision_explanation()
|
||||
let first = payload
|
||||
.as_array()
|
||||
.and_then(|items| items.first())
|
||||
.expect("job row");
|
||||
.unwrap_or_else(|| panic!("missing job row"));
|
||||
assert_eq!(
|
||||
first["decision_explanation"]["code"].as_str(),
|
||||
Some("bpp_below_threshold")
|
||||
@@ -1395,7 +1432,9 @@ async fn cancel_queued_job_updates_status() -> std::result::Result<(), Box<dyn s
|
||||
.await?;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let updated = state.db.get_job_by_id(job.id).await?.expect("updated job");
|
||||
let Some(updated) = state.db.get_job_by_id(job.id).await? else {
|
||||
panic!("missing updated job after cancel");
|
||||
};
|
||||
assert_eq!(updated.status, JobState::Cancelled);
|
||||
|
||||
cleanup_paths(&[input_path, output_path, config_path, db_path]);
|
||||
@@ -1422,7 +1461,9 @@ async fn priority_endpoint_updates_job_priority()
|
||||
let body = body_text(response).await;
|
||||
assert!(body.contains("\"priority\":10"));
|
||||
|
||||
let updated = state.db.get_job_by_id(job.id).await?.expect("updated job");
|
||||
let Some(updated) = state.db.get_job_by_id(job.id).await? else {
|
||||
panic!("missing updated job after priority update");
|
||||
};
|
||||
assert_eq!(updated.priority, 10);
|
||||
|
||||
cleanup_paths(&[input_path, output_path, config_path, db_path]);
|
||||
|
||||
@@ -641,7 +641,10 @@ mod tests {
|
||||
fn breadcrumbs_include_root_and_children() {
|
||||
let crumbs = breadcrumbs(Path::new("/media/movies"));
|
||||
assert!(!crumbs.is_empty());
|
||||
assert_eq!(crumbs.last().unwrap().path, "/media/movies");
|
||||
assert_eq!(
|
||||
crumbs.last().map(|crumb| crumb.path.as_str()),
|
||||
Some("/media/movies")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -662,14 +665,15 @@ mod tests {
|
||||
fn preview_detects_media_files_and_samples() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("alchemist_fs_preview_{}", rand::random::<u64>()));
|
||||
std::fs::create_dir_all(&root).expect("root");
|
||||
assert!(std::fs::create_dir_all(&root).is_ok());
|
||||
let media_file = root.join("movie.mkv");
|
||||
std::fs::write(&media_file, b"video").expect("media");
|
||||
assert!(std::fs::write(&media_file, b"video").is_ok());
|
||||
|
||||
let response = preview_blocking(FsPreviewRequest {
|
||||
directories: vec![root.to_string_lossy().to_string()],
|
||||
})
|
||||
.expect("preview");
|
||||
});
|
||||
assert!(response.is_ok());
|
||||
let response = response.unwrap_or_else(|err| panic!("preview failed: {err}"));
|
||||
|
||||
assert_eq!(response.total_media_files, 1);
|
||||
assert_eq!(response.directories.len(), 1);
|
||||
|
||||
@@ -1449,13 +1449,19 @@ mod tests {
|
||||
device_path: None,
|
||||
}],
|
||||
)));
|
||||
assert_eq!(state.snapshot().await.unwrap().vendor, Vendor::Nvidia);
|
||||
assert_eq!(
|
||||
state.snapshot().await.map(|info| info.vendor),
|
||||
Some(Vendor::Nvidia)
|
||||
);
|
||||
|
||||
state
|
||||
.replace(Some(HardwareInfo::new(Vendor::Cpu, None, Vec::new())))
|
||||
.await;
|
||||
|
||||
assert_eq!(state.snapshot().await.unwrap().vendor, Vendor::Cpu);
|
||||
assert_eq!(
|
||||
state.snapshot().await.map(|info| info.vendor),
|
||||
Some(Vendor::Cpu)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1527,7 +1533,8 @@ mod tests {
|
||||
#[test]
|
||||
fn detect_hardware_with_runner_can_fall_back_to_cpu() {
|
||||
let runner = FakeRunner::default();
|
||||
let info = detect_hardware_with_runner(&runner, true).expect("cpu fallback");
|
||||
let info = detect_hardware_with_runner(&runner, true)
|
||||
.unwrap_or_else(|err| panic!("expected cpu fallback info: {err}"));
|
||||
assert_eq!(info.vendor, Vendor::Cpu);
|
||||
assert_eq!(info.probe_summary.succeeded, 0);
|
||||
assert!(!info.selection_reason.is_empty());
|
||||
@@ -1569,7 +1576,7 @@ mod tests {
|
||||
|
||||
let (selected, mode) =
|
||||
choose_best_candidate_set(&[nvidia.clone(), amd], Some(Vendor::Intel), false)
|
||||
.expect("selected set");
|
||||
.unwrap_or_else(|| panic!("expected selected candidate set"));
|
||||
assert_eq!(mode, SelectionMode::Auto);
|
||||
assert_eq!(selected.vendor, Vendor::Nvidia);
|
||||
}
|
||||
@@ -1587,8 +1594,8 @@ mod tests {
|
||||
&[(HardwareBackend::Qsv, "hevc", "hevc_qsv")],
|
||||
);
|
||||
|
||||
let (selected, mode) =
|
||||
choose_best_candidate_set(&[qsv, vaapi.clone()], None, false).expect("selected set");
|
||||
let (selected, mode) = choose_best_candidate_set(&[qsv, vaapi.clone()], None, false)
|
||||
.unwrap_or_else(|| panic!("expected selected candidate set"));
|
||||
assert_eq!(mode, SelectionMode::Auto);
|
||||
assert_eq!(selected.device_path, vaapi.device_path);
|
||||
}
|
||||
@@ -1606,7 +1613,7 @@ mod tests {
|
||||
let results = collect_probe_results_verbose(&runner, candidates, &mut probe_log);
|
||||
let successful_sets = build_successful_candidate_sets(&results);
|
||||
let (selected, _) = choose_best_candidate_set(&successful_sets, Some(Vendor::Intel), false)
|
||||
.expect("selected set");
|
||||
.unwrap_or_else(|| panic!("expected selected candidate set"));
|
||||
mark_selected_probe_entries(&mut probe_log, &selected);
|
||||
|
||||
assert!(
|
||||
@@ -1636,8 +1643,10 @@ mod tests {
|
||||
let temp_root = std::env::temp_dir();
|
||||
let qsv_path = temp_root.join(format!("alchemist_qsv_{}", rand::random::<u64>()));
|
||||
let vaapi_path = temp_root.join(format!("alchemist_vaapi_{}", rand::random::<u64>()));
|
||||
std::fs::write(&qsv_path, b"render").expect("create qsv path");
|
||||
std::fs::write(&vaapi_path, b"render").expect("create vaapi path");
|
||||
std::fs::write(&qsv_path, b"render")
|
||||
.unwrap_or_else(|err| panic!("failed to create qsv path: {err}"));
|
||||
std::fs::write(&vaapi_path, b"render")
|
||||
.unwrap_or_else(|err| panic!("failed to create vaapi path: {err}"));
|
||||
|
||||
let qsv_runner = FakeRunner::with_successful_encoders(&["av1_qsv"]);
|
||||
let qsv_info = detect_explicit_device_path_with_runner(
|
||||
@@ -1645,7 +1654,7 @@ mod tests {
|
||||
qsv_path.to_string_lossy().as_ref(),
|
||||
Some(Vendor::Intel),
|
||||
)
|
||||
.expect("qsv info");
|
||||
.unwrap_or_else(|| panic!("expected qsv info"));
|
||||
assert_eq!(qsv_info.vendor, Vendor::Intel);
|
||||
|
||||
let vaapi_runner = FakeRunner::with_successful_encoders(&["hevc_vaapi"]);
|
||||
@@ -1654,7 +1663,7 @@ mod tests {
|
||||
vaapi_path.to_string_lossy().as_ref(),
|
||||
Some(Vendor::Amd),
|
||||
)
|
||||
.expect("vaapi info");
|
||||
.unwrap_or_else(|| panic!("expected vaapi info"));
|
||||
assert_eq!(vaapi_info.vendor, Vendor::Amd);
|
||||
|
||||
let _ = std::fs::remove_file(qsv_path);
|
||||
@@ -1669,7 +1678,8 @@ mod tests {
|
||||
"alchemist_explicit_failure_{}",
|
||||
rand::random::<u64>()
|
||||
));
|
||||
std::fs::write(&missing_path, b"render").expect("create explicit device path");
|
||||
std::fs::write(&missing_path, b"render")
|
||||
.unwrap_or_else(|err| panic!("failed to create explicit device path: {err}"));
|
||||
|
||||
let runner = FakeRunner::default();
|
||||
let info = detect_explicit_device_path_with_runner(
|
||||
@@ -1689,15 +1699,20 @@ mod tests {
|
||||
std::env::temp_dir().join(format!("alchemist_render_enum_{}", rand::random::<u64>()));
|
||||
let sys_root = temp_root.join("sys/class/drm");
|
||||
let dev_root = temp_root.join("dev/dri");
|
||||
std::fs::create_dir_all(sys_root.join("renderD128/device")).expect("create intel sys path");
|
||||
std::fs::create_dir_all(sys_root.join("renderD129/device")).expect("create amd sys path");
|
||||
std::fs::create_dir_all(&dev_root).expect("create dev root");
|
||||
std::fs::create_dir_all(sys_root.join("renderD128/device"))
|
||||
.unwrap_or_else(|err| panic!("failed to create intel sys path: {err}"));
|
||||
std::fs::create_dir_all(sys_root.join("renderD129/device"))
|
||||
.unwrap_or_else(|err| panic!("failed to create amd sys path: {err}"));
|
||||
std::fs::create_dir_all(&dev_root)
|
||||
.unwrap_or_else(|err| panic!("failed to create dev root: {err}"));
|
||||
std::fs::write(sys_root.join("renderD128/device/vendor"), "0x8086")
|
||||
.expect("write intel vendor");
|
||||
.unwrap_or_else(|err| panic!("failed to write intel vendor: {err}"));
|
||||
std::fs::write(sys_root.join("renderD129/device/vendor"), "0x1002")
|
||||
.expect("write amd vendor");
|
||||
std::fs::write(dev_root.join("renderD128"), b"render").expect("write intel render node");
|
||||
std::fs::write(dev_root.join("renderD129"), b"render").expect("write amd render node");
|
||||
.unwrap_or_else(|err| panic!("failed to write amd vendor: {err}"));
|
||||
std::fs::write(dev_root.join("renderD128"), b"render")
|
||||
.unwrap_or_else(|err| panic!("failed to write intel render node: {err}"));
|
||||
std::fs::write(dev_root.join("renderD129"), b"render")
|
||||
.unwrap_or_else(|err| panic!("failed to write amd render node: {err}"));
|
||||
|
||||
let nodes = enumerate_linux_render_nodes_under(&sys_root, &dev_root);
|
||||
assert_eq!(nodes.len(), 2);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "alchemist-web-e2e",
|
||||
"version": "0.3.0-rc.2",
|
||||
"version": "0.3.0-rc.3",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "alchemist-web",
|
||||
"version": "0.3.0-rc.2",
|
||||
"version": "0.3.0-rc.3",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"type": "module",
|
||||
|
||||
Reference in New Issue
Block a user