Files
wayscriber/tests/cli.rs
T
devmobasa b7bfbd6df6 fix(broker): validate inherited descriptors before borrowing them
Borrowing a descriptor requires it to be open at that moment, so the
broker entry cannot check a raw number through a borrow. Check both
numbers with getsockopt first, as main did, which keeps the rejection of
closed descriptors and makes the borrow's safety claim true.

The entry exits 126 either way, so this only restores the contract. Pin
that rejection with an integration test against the real binary.
2026-10-30 22:53:29 +01:00

902 lines
28 KiB
Rust

use std::ffi::OsString;
#[cfg(unix)]
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::process::Stdio;
use std::process::{Command, Output};
#[cfg(unix)]
use std::thread;
#[cfg(unix)]
use std::time::{Duration, Instant};
use std::{env, fs};
use wayscriber::env_vars::{
HOME_ENV, NO_DETACH_ENV, WAYLAND_DISPLAY_ENV, XDG_CONFIG_HOME_ENV, XDG_CURRENT_DESKTOP_ENV,
XDG_DATA_HOME_ENV, XDG_RUNTIME_DIR_ENV, XDG_SESSION_DESKTOP_ENV,
};
#[cfg(unix)]
use wayscriber::env_vars::{
NO_TRAY_ENV, PORTAL_APP_ID_ENV, PORTAL_SHORTCUT_ENV, PORTAL_SHORTCUT_OPT_IN_ENV, RUST_LOG_ENV,
};
use wayscriber::runtime_capabilities::RUNTIME_CAPABILITIES_FLAG;
use tempfile::TempDir;
use wayscriber::config::Config;
use wayscriber::draw::{Color, EraserKind, FontDescriptor, Frame, Shape};
use wayscriber::input::EraserMode;
use wayscriber::session::{
BoardPagesSnapshot, BoardSnapshot, SessionOptions, SessionSnapshot, ToolStateSnapshot,
};
use wayscriber::{build_info, draw, runtime_capabilities, session, shortcut_hint};
struct CommandOutput {
output: Output,
}
impl CommandOutput {
fn code(self, expected: i32) -> Self {
assert_eq!(
self.output.status.code(),
Some(expected),
"unexpected exit code\nstdout:\n{}\nstderr:\n{}",
self.stdout_text(),
self.stderr_text()
);
self
}
fn success(self) -> Self {
assert!(
self.output.status.success(),
"expected success\nstdout:\n{}\nstderr:\n{}",
self.stdout_text(),
self.stderr_text()
);
self
}
fn failure(self) -> Self {
assert!(
!self.output.status.success(),
"expected failure\nstdout:\n{}\nstderr:\n{}",
self.stdout_text(),
self.stderr_text()
);
self
}
fn stdout_contains(self, needle: &str) -> Self {
let stdout = self.stdout_text();
assert!(
stdout.contains(needle),
"stdout did not contain {needle:?}\nstdout:\n{stdout}\nstderr:\n{}",
self.stderr_text()
);
self
}
fn stdout_starts_with(self, prefix: &str) -> Self {
let stdout = self.stdout_text();
assert!(
stdout.starts_with(prefix),
"stdout did not start with {prefix:?}\nstdout:\n{stdout}\nstderr:\n{}",
self.stderr_text()
);
self
}
fn stdout_eq(self, expected: &str) -> Self {
let stdout = self.stdout_text();
assert_eq!(stdout, expected, "stderr:\n{}", self.stderr_text());
self
}
fn stderr_contains(self, needle: &str) -> Self {
let stderr = self.stderr_text();
assert!(
stderr.contains(needle),
"stderr did not contain {needle:?}\nstdout:\n{}\nstderr:\n{stderr}",
self.stdout_text()
);
self
}
fn stdout_text(&self) -> String {
String::from_utf8_lossy(&self.output.stdout).into_owned()
}
fn stderr_text(&self) -> String {
String::from_utf8_lossy(&self.output.stderr).into_owned()
}
}
fn run_command(command: &mut Command) -> CommandOutput {
CommandOutput {
output: command.output().expect("run wayscriber command"),
}
}
#[cfg(unix)]
fn run_command_with_timeout(
command: &mut Command,
temp: &TempDir,
timeout: Duration,
) -> CommandOutput {
let stdout_path = temp.path().join("child.stdout");
let stderr_path = temp.path().join("child.stderr");
let stdout = fs::File::create(&stdout_path).expect("create child stdout file");
let stderr = fs::File::create(&stderr_path).expect("create child stderr file");
let command_context = format!("{command:?}");
let mut child = command
.stdout(Stdio::from(stdout))
.stderr(Stdio::from(stderr))
.spawn()
.expect("spawn wayscriber command");
let deadline = Instant::now() + timeout;
loop {
match child.try_wait() {
Ok(Some(status)) => {
return CommandOutput {
output: Output {
status,
stdout: fs::read(&stdout_path).unwrap_or_default(),
stderr: fs::read(&stderr_path).unwrap_or_default(),
},
};
}
Ok(None) if Instant::now() < deadline => {
thread::sleep(Duration::from_millis(10));
}
Ok(None) => {
let kill_result = child.kill();
let wait_result = child.wait();
let stdout = fs::read_to_string(&stdout_path).unwrap_or_default();
let stderr = fs::read_to_string(&stderr_path).unwrap_or_default();
panic!(
"wayscriber child exceeded {timeout:?}\ncommand: {command_context}\nkill: {kill_result:?}\nwait: {wait_result:?}\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
}
Err(err) => {
let _ = child.kill();
let _ = child.wait();
panic!("failed to poll wayscriber child ({command_context}): {err}");
}
}
}
}
fn write_session_config(temp: &TempDir, custom_dir: &Path) {
let config_dir = temp.path().join("wayscriber");
fs::create_dir_all(&config_dir).expect("create test config directory");
let config_contents = format!(
r#"
[session]
persist_transparent = true
persist_whiteboard = false
persist_blackboard = false
restore_tool_state = true
storage = "custom"
custom_directory = "{}"
max_shapes_per_frame = 100
max_file_size_mb = 5
compress = "off"
auto_compress_threshold_kb = 100
backup_retention = 1
"#,
custom_dir.display()
);
fs::write(config_dir.join("config.toml"), config_contents).expect("write test session config");
}
fn write_disabled_session_config(temp: &TempDir, custom_dir: &Path) {
let config_dir = temp.path().join("wayscriber");
fs::create_dir_all(&config_dir).expect("create test config directory");
let config_contents = format!(
r#"
[session]
persist_transparent = false
persist_whiteboard = false
persist_blackboard = false
persist_history = false
restore_tool_state = false
storage = "custom"
custom_directory = "{}"
max_shapes_per_frame = 100
max_file_size_mb = 5
compress = "off"
auto_compress_threshold_kb = 100
backup_retention = 1
"#,
custom_dir.display()
);
fs::write(config_dir.join("config.toml"), config_contents).expect("write test session config");
}
fn saved_tool_state() -> ToolStateSnapshot {
ToolStateSnapshot {
current_color: Color {
r: 0.0,
g: 1.0,
b: 0.0,
a: 1.0,
},
current_thickness: 3.0,
eraser_size: 12.0,
eraser_kind: EraserKind::Circle,
eraser_mode: EraserMode::Brush,
blur_style: Default::default(),
recent_colors: Vec::new(),
pen_smoothing: None,
shape_recognition_sensitivity: None,
marker_opacity: Some(0.32),
spotlight_magnification: None,
fill_enabled: Some(false),
tool_override: None,
current_font_size: 24.0,
font_descriptor: Some(FontDescriptor::default()),
text_background_enabled: false,
arrow_length: 20.0,
arrow_angle: 30.0,
arrow_head_at_end: Some(false),
arrow_style: None,
arrow_label_enabled: Some(false),
polygon_sides: draw::REGULAR_POLYGON_DEFAULT_SIDES,
board_previous_color: None,
tool_settings: None,
}
}
fn saved_line_snapshot(with_tool_state: bool) -> SessionSnapshot {
let mut frame = Frame::new();
frame.add_shape(Shape::Line {
x1: 0,
y1: 0,
x2: 10,
y2: 10,
color: Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
thick: 2.0,
});
SessionSnapshot {
active_board_id: "transparent".to_string(),
boards: vec![BoardSnapshot {
appearance: None,
id: "transparent".to_string(),
pages: BoardPagesSnapshot {
pages: vec![frame],
active: 0,
},
}],
tool_state: with_tool_state.then(saved_tool_state),
}
}
fn wayscriber_cmd() -> Command {
Command::new(env!("CARGO_BIN_EXE_wayscriber"))
}
#[test]
fn wayscriber_help_prints_usage() {
run_command(wayscriber_cmd().arg("--help"))
.success()
.code(0)
.stdout_contains("Screen annotation tool for Wayland compositors")
.stdout_contains("--light-toggle Toggle light passthrough mode")
.stdout_contains("--light-draw-toggle")
.stdout_contains("--light-draw-on")
.stdout_contains("--light-draw-off");
}
#[test]
fn wayscriber_version_prints_binary_name() {
for arg in ["--version", "-V"] {
run_command(wayscriber_cmd().arg(arg))
.success()
.code(0)
.stdout_starts_with("wayscriber ")
.stdout_contains(build_info::version());
}
}
#[test]
fn wayscriber_runtime_capabilities_reports_portal_feature() {
let expected = runtime_capabilities::render_runtime_capabilities(
runtime_capabilities::current_runtime_capabilities(),
);
assert!(
expected.starts_with(&format!(
"portal={}\n",
shortcut_hint::portal_runtime_supported()
)),
"the rendered report must lead with the portal capability"
);
run_command(wayscriber_cmd().arg(RUNTIME_CAPABILITIES_FLAG))
.success()
.stdout_eq(&expected);
}
#[test]
fn bare_usage_mentions_freeze_on_show() {
run_command(&mut wayscriber_cmd())
.success()
.code(0)
.stdout_contains("--freeze-on-show")
.stdout_contains("--daemon-toggle");
}
#[test]
fn clustered_help_and_version_exit_zero_without_runtime_startup() {
run_command(wayscriber_cmd().env_remove(WAYLAND_DISPLAY_ENV).arg("-dh"))
.success()
.code(0)
.stdout_contains("Screen annotation tool for Wayland compositors");
run_command(wayscriber_cmd().env_remove(WAYLAND_DISPLAY_ENV).arg("-aV"))
.success()
.code(0)
.stdout_starts_with("wayscriber ");
}
#[cfg(unix)]
#[test]
fn internal_broker_entry_rejects_descriptors_that_are_not_broker_sockets() {
run_command(
wayscriber_cmd()
.env("WAYSCRIBER_INTERNAL_PROCESS_BROKER_FD", "3")
.env("WAYSCRIBER_INTERNAL_PROCESS_BROKER_SHUTDOWN_FD", "4")
.env("WAYSCRIBER_INTERNAL_PROCESS_BROKER_TOKEN", "a".repeat(64)),
)
.failure()
.code(126);
}
#[test]
fn invalid_arguments_exit_two() {
for (args, diagnostic) in [
(vec!["--not-a-real-option"], "unknown argument"),
(vec!["--mode"], "--mode requires a value"),
] {
run_command(wayscriber_cmd().args(args))
.failure()
.code(2)
.stderr_contains(diagnostic)
.stderr_contains("Try 'wayscriber --help' for usage.");
}
}
#[test]
fn active_mode_requires_wayland_env() {
run_command(
wayscriber_cmd()
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--active"),
)
.failure()
.code(1)
.stderr_contains(&format!("{WAYLAND_DISPLAY_ENV} not set"));
}
#[cfg(unix)]
#[test]
fn daemon_toggle_without_a_daemon_reports_that_it_is_not_running() {
let temp = TempDir::new().unwrap();
let runtime_dir = temp.path().join("runtime");
fs::create_dir_all(&runtime_dir).unwrap();
let mut command = wayscriber_cmd();
command
.env(XDG_RUNTIME_DIR_ENV, &runtime_dir)
.env(XDG_CONFIG_HOME_ENV, temp.path().join("config"))
.env(XDG_DATA_HOME_ENV, temp.path().join("data"))
.env(HOME_ENV, temp.path().join("home"))
.env(RUST_LOG_ENV, "off")
.arg("--daemon-toggle");
run_command_with_timeout(&mut command, &temp, Duration::from_secs(5))
.failure()
.code(1)
.stderr_contains("wayscriber daemon is not running");
}
#[cfg(unix)]
#[test]
fn daemon_lock_conflict_exits_seventy_five_without_starting_runtime_services() {
let temp = TempDir::new().unwrap();
let runtime_dir = temp.path().join("runtime");
let config_dir = temp.path().join("config");
let data_dir = temp.path().join("data");
let home_dir = temp.path().join("home");
for path in [&runtime_dir, &config_dir, &data_dir, &home_dir] {
fs::create_dir_all(path).unwrap();
}
let lock_dir = runtime_dir.join("wayscriber");
fs::create_dir_all(&lock_dir).unwrap();
let lock_path = lock_dir.join("wayscriber.lock");
let lock_file = fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&lock_path)
.unwrap();
session::try_lock_exclusive(&lock_file).unwrap();
let mut command = wayscriber_cmd();
command
.env(XDG_RUNTIME_DIR_ENV, &runtime_dir)
.env(XDG_CONFIG_HOME_ENV, &config_dir)
.env(XDG_DATA_HOME_ENV, &data_dir)
.env(HOME_ENV, &home_dir)
.env(WAYLAND_DISPLAY_ENV, "wayscriber-cli-test")
.env(NO_TRAY_ENV, "1")
.env(RUST_LOG_ENV, "off")
.env(XDG_CURRENT_DESKTOP_ENV, "GNOME")
.env(XDG_SESSION_DESKTOP_ENV, "GNOME")
.env_remove(PORTAL_SHORTCUT_OPT_IN_ENV)
.env_remove(PORTAL_SHORTCUT_ENV)
.env_remove(PORTAL_APP_ID_ENV)
.args(["--daemon", "--no-tray"]);
run_command_with_timeout(&mut command, &temp, Duration::from_secs(5))
.failure()
.code(75)
.stderr_contains("wayscriber daemon is already running");
drop(lock_file);
}
#[test]
fn session_clear_command_succeeds_without_files() {
let temp = TempDir::new().unwrap();
let session_dir = temp.path().join("sessions");
write_session_config(&temp, &session_dir);
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--clear-session"),
)
.success()
.stdout_contains("Session file:")
.stdout_contains("No session file present");
}
#[test]
fn rename_session_ignores_invalid_configuration() {
let temp = TempDir::new().unwrap();
let config_dir = temp.path().join("config");
let data_dir = temp.path().join("data");
let session = temp.path().join("lecture.wayscriber-session");
let catalog_dir = data_dir.join("wayscriber");
fs::create_dir_all(config_dir.join("wayscriber")).unwrap();
fs::create_dir_all(&catalog_dir).unwrap();
fs::write(
config_dir.join("wayscriber/config.toml"),
b"not valid toml = [",
)
.unwrap();
fs::write(&session, b"{}").unwrap();
let catalog_path = catalog_dir.join("sessions.json");
let catalog = serde_json::json!({
"version": 1,
"sessions": [{
"id": "s-test",
"display_name": "Before",
"path": session.to_str().unwrap(),
"canonical_path": session.canonicalize().unwrap().to_str().unwrap(),
"created_at_millis": 1,
"last_opened_at_millis": 1,
"last_saved_at_millis": null
}]
});
fs::write(&catalog_path, serde_json::to_vec(&catalog).unwrap()).unwrap();
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, &config_dir)
.env(XDG_DATA_HOME_ENV, &data_dir)
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--rename-session")
.arg("Lecture 04")
.arg("--session-file")
.arg(&session),
)
.success()
.stdout_contains("Renamed session to Lecture 04.");
let updated: serde_json::Value =
serde_json::from_slice(&fs::read(catalog_path).unwrap()).unwrap();
assert_eq!(updated["sessions"][0]["display_name"], "Lecture 04");
}
#[test]
fn named_session_info_missing_parent_reports_not_found() {
let temp = TempDir::new().unwrap();
let named_path = temp
.path()
.join("missing")
.join("lecture-04.wayscriber-session");
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--session-info")
.arg("--session-file")
.arg(&named_path),
)
.success()
.stdout_contains(&format!("Session file : {}", named_path.display()))
.stdout_contains("(not found)");
}
#[test]
fn named_session_info_forces_persistence_despite_disabled_config() {
let temp = TempDir::new().unwrap();
let session_dir = temp.path().join("sessions");
let named_path = temp.path().join("lecture-04.wayscriber-session");
write_disabled_session_config(&temp, &session_dir);
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--session-info")
.arg("--session-file")
.arg(&named_path),
)
.success()
.stdout_contains("Persist transparent: true")
.stdout_contains("Persist whiteboard : true")
.stdout_contains("Persist blackboard : true")
.stdout_contains("Persist history : true")
.stdout_contains("Restore tool state : true");
}
#[test]
fn named_session_clear_missing_parent_reports_no_artifacts() {
let temp = TempDir::new().unwrap();
let named_path = temp
.path()
.join("missing")
.join("lecture-04.wayscriber-session");
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--clear-session")
.arg("--session-file")
.arg(&named_path),
)
.success()
.stdout_contains(&format!("Session file: {}", named_path.display()))
.stdout_contains("No session artefacts found");
}
#[test]
fn clear_tool_state_command_succeeds_without_files() {
let temp = TempDir::new().unwrap();
let session_dir = temp.path().join("sessions");
write_session_config(&temp, &session_dir);
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--clear-tool-state"),
)
.success()
.stdout_contains("Session file:")
.stdout_contains("No session file present");
}
#[test]
fn clear_tool_state_named_session_targets_only_requested_file() {
let temp = TempDir::new().unwrap();
let selected = temp.path().join("lecture-04.wayscriber-session");
let sibling = temp.path().join("lecture-05.wayscriber-session");
let mut selected_options = SessionOptions::new(temp.path().to_path_buf(), "display");
selected_options.set_named_file_target(selected.clone());
selected_options.persist_transparent = true;
selected_options.restore_tool_state = true;
let mut sibling_options = SessionOptions::new(temp.path().to_path_buf(), "display");
sibling_options.set_named_file_target(sibling);
sibling_options.persist_transparent = true;
sibling_options.restore_tool_state = true;
session::save_snapshot(&saved_line_snapshot(true), &selected_options).unwrap();
session::save_snapshot(&saved_line_snapshot(true), &sibling_options).unwrap();
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--clear-tool-state")
.arg("--session-file")
.arg(&selected),
)
.success()
.stdout_contains(&format!("Session file: {}", selected.display()))
.stdout_contains("Cleared saved tool state")
.stdout_contains("Preserved saved boards and history");
let selected_loaded = session::load_snapshot(&selected_options)
.unwrap()
.expect("selected session should remain");
let sibling_loaded = session::load_snapshot(&sibling_options)
.unwrap()
.expect("sibling session should remain");
assert!(selected_loaded.tool_state.is_none());
assert!(sibling_loaded.tool_state.is_some());
}
#[cfg(unix)]
#[test]
fn named_session_info_rejects_symlink_primary() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("real-session.wayscriber-session");
let link = temp.path().join("linked-session.wayscriber-session");
fs::write(&target, b"{}").unwrap();
symlink(&target, &link).unwrap();
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--session-info")
.arg("--session-file")
.arg(&link),
)
.failure()
.stderr_contains("not a symlink");
}
#[cfg(unix)]
#[test]
fn named_session_clear_rejects_symlink_primary_without_removing_artifacts() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("real-session.wayscriber-session");
let link = temp.path().join("linked-session.wayscriber-session");
let mut backup_raw = OsString::from(link.as_os_str());
backup_raw.push(".bak");
let backup = PathBuf::from(backup_raw);
fs::write(&target, b"target").unwrap();
fs::write(&backup, b"backup").unwrap();
symlink(&target, &link).unwrap();
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--clear-session")
.arg("--session-file")
.arg(&link),
)
.failure()
.stderr_contains("not a symlink");
assert!(
fs::symlink_metadata(&link)
.unwrap()
.file_type()
.is_symlink()
);
assert_eq!(fs::read(&target).unwrap(), b"target");
assert_eq!(fs::read(&backup).unwrap(), b"backup");
}
#[cfg(unix)]
#[test]
fn active_named_session_rejects_symlink_primary_before_wayland_preflight() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("real-session.wayscriber-session");
let link = temp.path().join("linked-session.wayscriber-session");
fs::write(&target, b"{}").unwrap();
symlink(&target, &link).unwrap();
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env(NO_DETACH_ENV, "1")
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--active")
.arg("--session-file")
.arg(&link),
)
.failure()
.stderr_contains("not a symlink");
}
#[cfg(unix)]
#[test]
fn freeze_named_session_rejects_symlink_primary_before_wayland_preflight() {
let temp = TempDir::new().unwrap();
let target = temp.path().join("real-session.wayscriber-session");
let link = temp.path().join("linked-session.wayscriber-session");
fs::write(&target, b"{}").unwrap();
symlink(&target, &link).unwrap();
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env(NO_DETACH_ENV, "1")
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--freeze")
.arg("--session-file")
.arg(&link),
)
.failure()
.stderr_contains("not a symlink");
}
#[test]
fn active_named_session_missing_parent_fails_before_wayland_preflight() {
let temp = TempDir::new().unwrap();
let named_path = temp
.path()
.join("missing")
.join("lecture-04.wayscriber-session");
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--active")
.arg("--session-file")
.arg(&named_path),
)
.failure()
.stderr_contains("named session parent directory does not exist")
.stderr_contains(&named_path.parent().unwrap().display().to_string());
}
#[test]
fn active_named_session_directory_path_fails_before_wayland_preflight() {
let temp = TempDir::new().unwrap();
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--active")
.arg("--session-file")
.arg(temp.path()),
)
.failure()
.stderr_contains("--session-file must name a session file, not a directory");
}
#[test]
fn freeze_named_session_missing_parent_fails_before_wayland_preflight() {
let temp = TempDir::new().unwrap();
let named_path = temp
.path()
.join("missing")
.join("lecture-04.wayscriber-session");
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--freeze")
.arg("--session-file")
.arg(&named_path),
)
.failure()
.stderr_contains("named session parent directory does not exist");
}
#[test]
fn freeze_named_session_missing_wayland_fails_before_detach() {
let temp = TempDir::new().unwrap();
let named_path = temp.path().join("lecture-04.wayscriber-session");
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env_remove(WAYLAND_DISPLAY_ENV)
.arg("--freeze")
.arg("--session-file")
.arg(&named_path),
)
.failure()
.stderr_contains(&format!("{WAYLAND_DISPLAY_ENV} not set"));
}
#[test]
fn session_info_reports_saved_snapshot() {
let temp = TempDir::new().unwrap();
let session_dir = temp.path().join("sessions");
write_session_config(&temp, &session_dir);
let display = "test-session";
let original_config = env::var_os(XDG_CONFIG_HOME_ENV);
// SAFETY: no lock guards this write. This is the only test in this binary
// that changes the environment, and the tests that may run alongside it
// read the environment only through std (`Command` spawns), which
// serializes those reads with this call.
unsafe {
env::set_var(XDG_CONFIG_HOME_ENV, temp.path());
}
let original_display = env::var_os(WAYLAND_DISPLAY_ENV);
// SAFETY: as for `XDG_CONFIG_HOME` above.
unsafe {
env::set_var(WAYLAND_DISPLAY_ENV, display);
}
let loaded = Config::load().unwrap();
let config_dir = Config::config_directory_from_source(&loaded.source).unwrap();
let mut options =
session::options_from_config(&loaded.config.session, &config_dir, Some(display)).unwrap();
options.set_output_identity(Some("DP-1"));
// SAFETY: as for the writes above; the restore runs on the same thread.
unsafe {
match original_config {
Some(value) => env::set_var(XDG_CONFIG_HOME_ENV, value),
None => env::remove_var(XDG_CONFIG_HOME_ENV),
}
match original_display {
Some(value) => env::set_var(WAYLAND_DISPLAY_ENV, value),
None => env::remove_var(WAYLAND_DISPLAY_ENV),
}
}
let mut frame = Frame::new();
frame.add_shape(Shape::Line {
x1: 0,
y1: 0,
x2: 10,
y2: 10,
color: Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
},
thick: 2.0,
});
let snapshot = SessionSnapshot {
active_board_id: "transparent".to_string(),
boards: vec![BoardSnapshot {
appearance: None,
id: "transparent".to_string(),
pages: BoardPagesSnapshot {
pages: vec![frame],
active: 0,
},
}],
tool_state: None,
};
session::save_snapshot(&snapshot, &options).unwrap();
run_command(
wayscriber_cmd()
.env(XDG_CONFIG_HOME_ENV, temp.path())
.env(WAYLAND_DISPLAY_ENV, display)
.arg("--session-info"),
)
.success()
.stdout_contains("Per-output persistence: true")
.stdout_contains("Session file :")
.stdout_contains("Output identity: DP_1")
.stdout_contains("transparent 1")
.stdout_contains("Tool state stored: false");
}