Skip to main content

screen_record/
test_mode.rs

1use std::path::{Path, PathBuf};
2use std::sync::OnceLock;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::{Duration, Instant};
5
6use crate::cli::{ContainerFormat, ImageFormat};
7use crate::error::CliError;
8use crate::types::{AppInfo, DisplayInfo, Rect, ShareableContent, WindowInfo};
9use nils_common::env as shared_env;
10
11static CTRL_C_INSTALLED: OnceLock<Result<(), CliError>> = OnceLock::new();
12static STOP_REQUESTED: AtomicBool = AtomicBool::new(false);
13
14pub fn enabled() -> bool {
15    env_truthy("CODEX_SCREEN_RECORD_TEST_MODE")
16}
17
18pub fn shareable_content() -> ShareableContent {
19    let displays = vec![DisplayInfo {
20        id: 1,
21        width: 1440,
22        height: 900,
23    }];
24
25    let windows = vec![
26        WindowInfo {
27            id: 100,
28            owner_name: "Terminal".to_string(),
29            title: "Inbox".to_string(),
30            bounds: Rect {
31                x: 0,
32                y: 0,
33                width: 1200,
34                height: 800,
35            },
36            on_screen: true,
37            active: true,
38            owner_pid: 111,
39            z_order: 0,
40        },
41        WindowInfo {
42            id: 101,
43            owner_name: "Terminal".to_string(),
44            title: "Docs".to_string(),
45            bounds: Rect {
46                x: 40,
47                y: 40,
48                width: 1100,
49                height: 760,
50            },
51            on_screen: true,
52            active: false,
53            owner_pid: 111,
54            z_order: 1,
55        },
56        WindowInfo {
57            id: 200,
58            owner_name: "Finder".to_string(),
59            title: "Finder".to_string(),
60            bounds: Rect {
61                x: 80,
62                y: 80,
63                width: 900,
64                height: 600,
65            },
66            on_screen: true,
67            active: false,
68            owner_pid: 222,
69            z_order: 2,
70        },
71    ];
72
73    let apps = vec![
74        AppInfo {
75            name: "Terminal".to_string(),
76            pid: 111,
77            bundle_id: "com.apple.Terminal".to_string(),
78        },
79        AppInfo {
80            name: "Finder".to_string(),
81            pid: 222,
82            bundle_id: "com.apple.Finder".to_string(),
83        },
84    ];
85
86    ShareableContent {
87        displays,
88        windows,
89        apps,
90    }
91}
92
93pub fn record_fixture(path: &Path, format: ContainerFormat) -> Result<(), CliError> {
94    let source = fixture_path(format);
95    if !source.exists() {
96        return Err(CliError::runtime(format!(
97            "fixture not found: {}",
98            source.display()
99        )));
100    }
101
102    if let Some(parent) = path.parent() {
103        std::fs::create_dir_all(parent)
104            .map_err(|err| CliError::runtime(format!("failed to create output dir: {err}")))?;
105    }
106
107    std::fs::copy(&source, path)
108        .map_err(|err| CliError::runtime(format!("failed to write output: {err}")))?;
109    Ok(())
110}
111
112pub fn record_fixture_for_duration(
113    duration: u64,
114    path: &Path,
115    format: ContainerFormat,
116) -> Result<(), CliError> {
117    if realtime_recording_enabled() {
118        install_ctrlc_handler()?;
119        STOP_REQUESTED.store(false, Ordering::SeqCst);
120        wait_until_duration_or_stop(duration);
121    }
122
123    if fail_recording_after_partial_write() {
124        if let Some(parent) = path.parent() {
125            std::fs::create_dir_all(parent)
126                .map_err(|err| CliError::runtime(format!("failed to create output dir: {err}")))?;
127        }
128        std::fs::write(path, b"")
129            .map_err(|err| CliError::runtime(format!("failed to write output: {err}")))?;
130        return Err(CliError::runtime(
131            "failed to append sample buffer: The operation could not be completed",
132        ));
133    }
134
135    record_fixture(path, format)
136}
137
138pub fn screenshot_fixture(path: &Path, format: ImageFormat) -> Result<(), CliError> {
139    let source = screenshot_fixture_path(format);
140    if !source.exists() {
141        return Err(CliError::runtime(format!(
142            "fixture not found: {}",
143            source.display()
144        )));
145    }
146
147    if let Some(parent) = path.parent() {
148        std::fs::create_dir_all(parent)
149            .map_err(|err| CliError::runtime(format!("failed to create output dir: {err}")))?;
150    }
151
152    std::fs::copy(&source, path)
153        .map_err(|err| CliError::runtime(format!("failed to write output: {err}")))?;
154    Ok(())
155}
156
157pub struct TestWriter {
158    path: PathBuf,
159    format: ContainerFormat,
160    appended: bool,
161}
162
163impl TestWriter {
164    pub fn new(path: &Path, format: ContainerFormat) -> Self {
165        Self {
166            path: path.to_path_buf(),
167            format,
168            appended: false,
169        }
170    }
171
172    pub fn append_frame(&mut self, _data: &[u8]) -> Result<(), CliError> {
173        self.appended = true;
174        Ok(())
175    }
176
177    pub fn finish(self) -> Result<(), CliError> {
178        if !self.appended {
179            return Err(CliError::runtime("no frames appended"));
180        }
181        record_fixture(&self.path, self.format)
182    }
183}
184
185fn env_truthy(name: &str) -> bool {
186    let value = std::env::var_os(name).map(|raw| raw.to_string_lossy().into_owned());
187    shared_env::is_truthy_or(value.as_deref().map(str::trim), false)
188}
189
190fn realtime_recording_enabled() -> bool {
191    env_truthy("CODEX_SCREEN_RECORD_TEST_MODE_REALTIME")
192}
193
194fn fail_recording_after_partial_write() -> bool {
195    env_truthy("CODEX_SCREEN_RECORD_TEST_MODE_FAIL_APPEND")
196}
197
198fn install_ctrlc_handler() -> Result<(), CliError> {
199    CTRL_C_INSTALLED
200        .get_or_init(|| {
201            ctrlc::set_handler(|| {
202                STOP_REQUESTED.store(true, Ordering::SeqCst);
203            })
204            .map_err(|err| CliError::runtime(format!("failed to set Ctrl-C handler: {err}")))?;
205            Ok(())
206        })
207        .clone()
208}
209
210fn wait_until_duration_or_stop(duration: u64) {
211    let deadline = Instant::now() + Duration::from_secs(duration);
212    while Instant::now() < deadline {
213        if STOP_REQUESTED.load(Ordering::SeqCst) {
214            break;
215        }
216        std::thread::sleep(Duration::from_millis(25));
217    }
218}
219
220fn fixture_path(format: ContainerFormat) -> PathBuf {
221    let filename = match format {
222        ContainerFormat::Mov => "sample.mov",
223        ContainerFormat::Mp4 => "sample.mp4",
224    };
225    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
226        .join("tests")
227        .join("fixtures")
228        .join(filename)
229}
230
231fn screenshot_fixture_path(format: ImageFormat) -> PathBuf {
232    let filename = match format {
233        ImageFormat::Png => "sample.png",
234        ImageFormat::Jpg => "sample.jpg",
235        ImageFormat::Webp => "sample.webp",
236    };
237    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
238        .join("tests")
239        .join("fixtures")
240        .join(filename)
241}
242
243#[cfg(test)]
244mod tests {
245    use super::enabled;
246    use nils_test_support::{EnvGuard, GlobalStateLock};
247
248    #[test]
249    fn enabled_returns_false_when_env_missing() {
250        let lock = GlobalStateLock::new();
251        let _guard = EnvGuard::remove(&lock, "CODEX_SCREEN_RECORD_TEST_MODE");
252        assert!(!enabled());
253    }
254
255    #[test]
256    fn enabled_accepts_expected_truthy_values() {
257        let lock = GlobalStateLock::new();
258        for value in ["1", "true", " yes ", "ON"] {
259            let _guard = EnvGuard::set(&lock, "CODEX_SCREEN_RECORD_TEST_MODE", value);
260            assert!(enabled(), "expected truthy value: {value}");
261        }
262    }
263
264    #[test]
265    fn enabled_rejects_falsey_and_unknown_values() {
266        let lock = GlobalStateLock::new();
267        for value in ["", "0", "false", "no", "off", "y", "enabled"] {
268            let _guard = EnvGuard::set(&lock, "CODEX_SCREEN_RECORD_TEST_MODE", value);
269            assert!(!enabled(), "expected falsey value: {value}");
270        }
271    }
272}