@rehearsal-labs/retest 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/api/source-location.d.ts.map +1 -1
- package/dist/api/source-location.js +3 -1
- package/dist/browser/builds.d.ts +35 -6
- package/dist/browser/builds.d.ts.map +1 -1
- package/dist/browser/builds.js +115 -16
- package/dist/browser/cdp/transport.d.ts.map +1 -1
- package/dist/browser/cdp/transport.js +4 -4
- package/dist/browser/chromium-process.d.ts +23 -0
- package/dist/browser/chromium-process.d.ts.map +1 -1
- package/dist/browser/chromium-process.js +78 -13
- package/dist/browser/electron.d.ts +1 -1
- package/dist/browser/electron.d.ts.map +1 -1
- package/dist/browser/electron.js +6 -3
- package/dist/browser/executables.d.ts +16 -1
- package/dist/browser/executables.d.ts.map +1 -1
- package/dist/browser/executables.js +50 -18
- package/dist/browser/firefox/accessible-names.d.ts +5 -4
- package/dist/browser/firefox/accessible-names.d.ts.map +1 -1
- package/dist/browser/firefox/accessible-names.js +11 -6
- package/dist/browser/firefox/bridge.js +3 -2
- package/dist/browser/firefox/executable.d.ts +14 -14
- package/dist/browser/firefox/executable.d.ts.map +1 -1
- package/dist/browser/firefox/executable.js +29 -21
- package/dist/browser/firefox/launch.js +1 -1
- package/dist/browser/firefox/navigation.d.ts +5 -2
- package/dist/browser/firefox/navigation.d.ts.map +1 -1
- package/dist/browser/firefox/navigation.js +48 -14
- package/dist/browser/firefox/orphans.d.ts.map +1 -1
- package/dist/browser/firefox/orphans.js +34 -2
- package/dist/browser/firefox/page.d.ts.map +1 -1
- package/dist/browser/firefox/page.js +113 -19
- package/dist/browser/firefox/process.d.ts +20 -1
- package/dist/browser/firefox/process.d.ts.map +1 -1
- package/dist/browser/firefox/process.js +124 -22
- package/dist/browser/firefox/profile.d.ts +5 -0
- package/dist/browser/firefox/profile.d.ts.map +1 -1
- package/dist/browser/firefox/profile.js +17 -1
- package/dist/browser/firefox/storage.d.ts.map +1 -1
- package/dist/browser/firefox/storage.js +4 -2
- package/dist/browser/firefox/subscriptions.d.ts +26 -0
- package/dist/browser/firefox/subscriptions.d.ts.map +1 -0
- package/dist/browser/firefox/subscriptions.js +10 -0
- package/dist/browser/launch.d.ts +6 -0
- package/dist/browser/launch.d.ts.map +1 -1
- package/dist/browser/launch.js +14 -1
- package/dist/cli/commands/doctor.js +3 -3
- package/dist/cli/commands/inspect.js +3 -2
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/install.js +26 -13
- package/dist/cli/commands/licences.d.ts +22 -0
- package/dist/cli/commands/licences.d.ts.map +1 -1
- package/dist/cli/commands/licences.js +62 -20
- package/dist/cli/commands/run.js +2 -2
- package/dist/cli/doctor/checks.js +4 -1
- package/dist/cli/inspect/diagnostics.js +3 -3
- package/dist/cli/inspect/test-timeline.js +10 -10
- package/dist/cli/install/doctor-rows.d.ts.map +1 -1
- package/dist/cli/install/doctor-rows.js +5 -4
- package/dist/cli/install/file-changes.d.ts +44 -0
- package/dist/cli/install/file-changes.d.ts.map +1 -0
- package/dist/cli/install/file-changes.js +81 -0
- package/dist/cli/install/install-archive.d.ts +3 -1
- package/dist/cli/install/install-archive.d.ts.map +1 -1
- package/dist/cli/install/install-archive.js +80 -33
- package/dist/cli/install/lock.d.ts.map +1 -1
- package/dist/cli/install/lock.js +108 -21
- package/dist/cli/install/media-doctor.d.ts.map +1 -1
- package/dist/cli/install/media-doctor.js +2 -1
- package/dist/cli/install/media-install.d.ts.map +1 -1
- package/dist/cli/install/media-install.js +38 -19
- package/dist/cli/install/media-pins.d.ts +7 -1
- package/dist/cli/install/media-pins.d.ts.map +1 -1
- package/dist/cli/install/media-pins.js +23 -12
- package/dist/cli/install/media-record.d.ts.map +1 -1
- package/dist/cli/install/media-record.js +10 -9
- package/dist/cli/install/media-report.d.ts +3 -1
- package/dist/cli/install/media-report.d.ts.map +1 -1
- package/dist/cli/install/media-report.js +7 -5
- package/dist/cli/install/media-tools.d.ts.map +1 -1
- package/dist/cli/install/media-tools.js +6 -4
- package/dist/cli/install/process-start.d.ts +3 -2
- package/dist/cli/install/process-start.d.ts.map +1 -1
- package/dist/cli/install/process-start.js +4 -0
- package/dist/cli/install/report.d.ts.map +1 -1
- package/dist/cli/install/report.js +11 -7
- package/dist/cli/install/supervisor-install.d.ts +33 -0
- package/dist/cli/install/supervisor-install.d.ts.map +1 -0
- package/dist/cli/install/supervisor-install.js +158 -0
- package/dist/cli/install/supervisor-locate.d.ts +47 -0
- package/dist/cli/install/supervisor-locate.d.ts.map +1 -0
- package/dist/cli/install/supervisor-locate.js +113 -0
- package/dist/cli/install/supervisor-pins.d.ts +21 -0
- package/dist/cli/install/supervisor-pins.d.ts.map +1 -0
- package/dist/cli/install/supervisor-pins.js +28 -0
- package/dist/cli/install/supervisor-record.d.ts +31 -0
- package/dist/cli/install/supervisor-record.d.ts.map +1 -0
- package/dist/cli/install/supervisor-record.js +104 -0
- package/dist/cli/install/supervisor-report.d.ts +11 -0
- package/dist/cli/install/supervisor-report.d.ts.map +1 -0
- package/dist/cli/install/supervisor-report.js +33 -0
- package/dist/cli/install/supervisor-tools.d.ts +43 -0
- package/dist/cli/install/supervisor-tools.d.ts.map +1 -0
- package/dist/cli/install/supervisor-tools.js +114 -0
- package/dist/cli/install/unpack.d.ts +23 -3
- package/dist/cli/install/unpack.d.ts.map +1 -1
- package/dist/cli/install/unpack.js +124 -10
- package/dist/cli/install/zip-member.d.ts +15 -0
- package/dist/cli/install/zip-member.d.ts.map +1 -0
- package/dist/cli/install/zip-member.js +77 -0
- package/dist/cli/interrupt.d.ts +1 -1
- package/dist/cli/interrupt.d.ts.map +1 -1
- package/dist/cli/interrupt.js +15 -10
- package/dist/cli/terminal.d.ts +9 -1
- package/dist/cli/terminal.d.ts.map +1 -1
- package/dist/cli/terminal.js +14 -0
- package/dist/config/define.d.ts +2 -2
- package/dist/config/define.js +2 -2
- package/dist/diagnostics/firefox-collector.d.ts +11 -10
- package/dist/diagnostics/firefox-collector.d.ts.map +1 -1
- package/dist/diagnostics/firefox-collector.js +16 -11
- package/dist/media/client.d.ts +2 -1
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/client.js +16 -5
- package/dist/media/host.d.ts +32 -0
- package/dist/media/host.d.ts.map +1 -0
- package/dist/media/host.js +50 -0
- package/dist/media/locate.d.ts.map +1 -1
- package/dist/media/locate.js +4 -3
- package/dist/native/processes.d.ts.map +1 -1
- package/dist/native/processes.js +7 -3
- package/dist/protocol/files.d.ts +1 -1
- package/dist/protocol/files.d.ts.map +1 -1
- package/dist/protocol/files.js +10 -5
- package/dist/reporters/commands.d.ts +12 -3
- package/dist/reporters/commands.d.ts.map +1 -1
- package/dist/reporters/commands.js +29 -11
- package/dist/reporters/human-card.js +1 -1
- package/dist/reporters/human.d.ts +3 -1
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +8 -8
- package/dist/reporters/style.d.ts +21 -2
- package/dist/reporters/style.d.ts.map +1 -1
- package/dist/reporters/style.js +11 -2
- package/dist/runner/app-server.d.ts +27 -7
- package/dist/runner/app-server.d.ts.map +1 -1
- package/dist/runner/app-server.js +74 -24
- package/dist/runner/child.js +6 -2
- package/dist/runner/media-leftovers.d.ts +7 -3
- package/dist/runner/media-leftovers.d.ts.map +1 -1
- package/dist/runner/media-leftovers.js +27 -6
- package/dist/runner/resources.d.ts +8 -0
- package/dist/runner/resources.d.ts.map +1 -1
- package/dist/runner/resources.js +12 -1
- package/dist/runner/run-session.d.ts.map +1 -1
- package/dist/runner/run-session.js +9 -4
- package/dist/runner/run.d.ts.map +1 -1
- package/dist/runner/run.js +2 -1
- package/dist/runner/target-drivers.d.ts +5 -2
- package/dist/runner/target-drivers.d.ts.map +1 -1
- package/dist/runner/target-drivers.js +10 -1
- package/dist/runner/test-file-process.d.ts +8 -0
- package/dist/runner/test-file-process.d.ts.map +1 -1
- package/dist/runner/test-file-process.js +26 -3
- package/dist/runner/test-pages.d.ts.map +1 -1
- package/dist/runner/test-pages.js +5 -2
- package/dist/shared/environment.d.ts +17 -0
- package/dist/shared/environment.d.ts.map +1 -0
- package/dist/shared/environment.js +27 -0
- package/dist/shared/metadata-process-worker.js +15 -7
- package/dist/shared/metadata-process.d.ts +5 -0
- package/dist/shared/metadata-process.d.ts.map +1 -1
- package/dist/shared/metadata-process.js +36 -6
- package/dist/shared/posix-path.d.ts +12 -0
- package/dist/shared/posix-path.d.ts.map +1 -1
- package/dist/shared/posix-path.js +16 -0
- package/dist/shared/private-file.d.ts +38 -0
- package/dist/shared/private-file.d.ts.map +1 -0
- package/dist/shared/private-file.js +132 -0
- package/dist/shared/process-ownership.d.ts +52 -2
- package/dist/shared/process-ownership.d.ts.map +1 -1
- package/dist/shared/process-ownership.js +104 -11
- package/dist/shared/process-supervisor.d.ts +309 -0
- package/dist/shared/process-supervisor.d.ts.map +1 -0
- package/dist/shared/process-supervisor.js +737 -0
- package/dist/shared/process-system.d.ts +113 -0
- package/dist/shared/process-system.d.ts.map +1 -0
- package/dist/shared/process-system.js +254 -0
- package/dist/shared/regular-file.d.ts +2 -2
- package/dist/shared/regular-file.d.ts.map +1 -1
- package/dist/shared/regular-file.js +19 -2
- package/dist/store/artifacts.d.ts +5 -4
- package/dist/store/artifacts.d.ts.map +1 -1
- package/dist/store/artifacts.js +18 -5
- package/dist/store/run-store.d.ts +5 -2
- package/dist/store/run-store.d.ts.map +1 -1
- package/dist/store/run-store.js +48 -4
- package/media/Cargo.lock +16 -0
- package/media/Cargo.toml +16 -1
- package/media/src/encoder.rs +56 -12
- package/media/src/frame.rs +7 -1
- package/media/src/main.rs +20 -2
- package/media/src/place.rs +230 -11
- package/media/src/process_ownership_windows.rs +1058 -0
- package/media/src/recording.rs +15 -7
- package/media/src/replies.rs +55 -0
- package/media/src/server.rs +72 -2
- package/media/src/store.rs +71 -2
- package/package.json +9 -4
- package/src/cli/install/media-notices.txt +465 -1
- package/supervisor/Cargo.lock +25 -0
- package/supervisor/Cargo.toml +22 -0
- package/supervisor/NOTICES +464 -0
- package/supervisor/build.rs +44 -0
- package/supervisor/src/command.rs +612 -0
- package/supervisor/src/jobs.rs +1650 -0
- package/supervisor/src/main.rs +71 -0
- package/supervisor/src/reply.rs +367 -0
package/media/src/recording.rs
CHANGED
|
@@ -307,13 +307,14 @@ impl Recording {
|
|
|
307
307
|
plan.fps,
|
|
308
308
|
&plan.partial_path(),
|
|
309
309
|
);
|
|
310
|
-
let mut child
|
|
311
|
-
.map_err(
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
310
|
+
let (mut child, mut ownership) =
|
|
311
|
+
encoder::spawn_owned(ffmpeg, &arguments, Stdio::piped(), Stdio::null()).map_err(
|
|
312
|
+
|error| EncoderFailure {
|
|
313
|
+
message: format!("{} could not be started: {error}", ffmpeg.display()),
|
|
314
|
+
exit: None,
|
|
315
|
+
},
|
|
316
|
+
)?;
|
|
315
317
|
let encoder_pid = child.id();
|
|
316
|
-
let mut ownership = Ownership::new(encoder_pid);
|
|
317
318
|
let mut problems = ownership.capture();
|
|
318
319
|
if !problems.is_empty() {
|
|
319
320
|
drop(child.stdin.take());
|
|
@@ -1364,6 +1365,13 @@ fn finalize_within(
|
|
|
1364
1365
|
.stdin(Stdio::null())
|
|
1365
1366
|
.stdout(Stdio::piped())
|
|
1366
1367
|
.stderr(Stdio::null());
|
|
1368
|
+
// A media process started without a console would give the finalizer a console window of its own, which on a
|
|
1369
|
+
// desktop opens at each recording's end and can take the focus from a browser under test.
|
|
1370
|
+
#[cfg(windows)]
|
|
1371
|
+
{
|
|
1372
|
+
use std::os::windows::process::CommandExt;
|
|
1373
|
+
command.creation_flags(windows_sys::Win32::System::Threading::CREATE_NO_WINDOW);
|
|
1374
|
+
}
|
|
1367
1375
|
#[cfg(debug_assertions)]
|
|
1368
1376
|
for name in [
|
|
1369
1377
|
"RETEST_MEDIA_TEST_NO_HARD_LINKS",
|
|
@@ -1399,7 +1407,7 @@ fn finalize_within(
|
|
|
1399
1407
|
};
|
|
1400
1408
|
match status {
|
|
1401
1409
|
Some(status) => {
|
|
1402
|
-
#[cfg(debug_assertions)]
|
|
1410
|
+
#[cfg(all(debug_assertions, unix))]
|
|
1403
1411
|
if std::env::var_os("RETEST_MEDIA_TEST_KILL_DURING_COPY").is_some()
|
|
1404
1412
|
&& encoder::signal_of(status) == Some(9)
|
|
1405
1413
|
{
|
package/media/src/replies.rs
CHANGED
|
@@ -19,6 +19,7 @@ const ADMISSION_WAIT: Duration = Duration::from_millis(100);
|
|
|
19
19
|
const WRITER_CLOSE_WAIT: Duration = Duration::from_millis(500);
|
|
20
20
|
|
|
21
21
|
/// Duplicating stdout keeps a blocked writer away from Rust's global stdout shutdown flush.
|
|
22
|
+
#[cfg(unix)]
|
|
22
23
|
pub fn stdout_pipe() -> io::Result<std::fs::File> {
|
|
23
24
|
use std::os::fd::FromRawFd;
|
|
24
25
|
// SAFETY: F_DUPFD_CLOEXEC creates a separately owned descriptor, atomically closed on child exec. File
|
|
@@ -31,6 +32,30 @@ pub fn stdout_pipe() -> io::Result<std::fs::File> {
|
|
|
31
32
|
Ok(unsafe { std::fs::File::from_raw_fd(fd) })
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/// Duplicating stdout keeps a blocked writer away from Rust's global stdout shutdown flush. The duplicate cannot be
|
|
36
|
+
/// inherited. Windows gives a new process every inheritable handle of its parent, not only its standard ones, so the
|
|
37
|
+
/// standard handles the client gave this process stop being inheritable too: an encoder, or anything it leaves
|
|
38
|
+
/// running, then holds none of the client's pipes open.
|
|
39
|
+
#[cfg(windows)]
|
|
40
|
+
pub fn stdout_pipe() -> io::Result<std::fs::File> {
|
|
41
|
+
use std::os::windows::io::{AsHandle, AsRawHandle};
|
|
42
|
+
use windows_sys::Win32::Foundation::{HANDLE_FLAG_INHERIT, SetHandleInformation};
|
|
43
|
+
let streams = [
|
|
44
|
+
io::stdin().as_raw_handle(),
|
|
45
|
+
io::stdout().as_raw_handle(),
|
|
46
|
+
io::stderr().as_raw_handle(),
|
|
47
|
+
];
|
|
48
|
+
for handle in streams.into_iter().filter(|handle| !handle.is_null()) {
|
|
49
|
+
// SAFETY: a standard handle of this process; only its inheritance flag changes. A console handle that refuses
|
|
50
|
+
// the change was never inheritable by an encoder, which gets its own pipes and no console.
|
|
51
|
+
unsafe { SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0) };
|
|
52
|
+
}
|
|
53
|
+
// `try_clone_to_owned` duplicates without inheritance.
|
|
54
|
+
Ok(std::fs::File::from(
|
|
55
|
+
io::stdout().as_handle().try_clone_to_owned()?,
|
|
56
|
+
))
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
#[derive(Debug, Default, PartialEq, Eq)]
|
|
35
60
|
pub struct ReplyClose {
|
|
36
61
|
pub replies_dropped: u64,
|
|
@@ -332,6 +357,7 @@ mod tests {
|
|
|
332
357
|
})
|
|
333
358
|
}
|
|
334
359
|
|
|
360
|
+
#[cfg(unix)]
|
|
335
361
|
#[test]
|
|
336
362
|
fn the_writer_stdout_descriptor_is_closed_when_an_encoder_executes() {
|
|
337
363
|
use std::os::fd::AsRawFd;
|
|
@@ -347,6 +373,35 @@ mod tests {
|
|
|
347
373
|
);
|
|
348
374
|
}
|
|
349
375
|
|
|
376
|
+
#[cfg(windows)]
|
|
377
|
+
#[test]
|
|
378
|
+
fn the_writer_stdout_handle_and_the_standard_handles_are_not_inherited_by_an_encoder() {
|
|
379
|
+
use std::os::windows::io::AsRawHandle;
|
|
380
|
+
use windows_sys::Win32::Foundation::{GetHandleInformation, HANDLE_FLAG_INHERIT};
|
|
381
|
+
let flags_of = |handle| {
|
|
382
|
+
let mut flags = 0u32;
|
|
383
|
+
// SAFETY: GetHandleInformation only reads the flags of a live handle of this process.
|
|
384
|
+
let read = unsafe { GetHandleInformation(handle, &mut flags) };
|
|
385
|
+
assert_ne!(read, 0, "handle flags are readable");
|
|
386
|
+
flags
|
|
387
|
+
};
|
|
388
|
+
let output = stdout_pipe().expect("stdout duplicate");
|
|
389
|
+
// HANDLE_FLAG_INHERIT is the property that would let a lingering encoder retain the media pipe.
|
|
390
|
+
assert_eq!(
|
|
391
|
+
flags_of(output.as_raw_handle()) & HANDLE_FLAG_INHERIT,
|
|
392
|
+
0,
|
|
393
|
+
"an encoder child must never inherit the reply pipe"
|
|
394
|
+
);
|
|
395
|
+
let stdout = io::stdout().as_raw_handle();
|
|
396
|
+
if !stdout.is_null() {
|
|
397
|
+
assert_eq!(
|
|
398
|
+
flags_of(stdout) & HANDLE_FLAG_INHERIT,
|
|
399
|
+
0,
|
|
400
|
+
"nor the stdout the client reads"
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
350
405
|
#[test]
|
|
351
406
|
fn replies_are_written_whole_and_in_order() {
|
|
352
407
|
let output = Shared::default();
|
package/media/src/server.rs
CHANGED
|
@@ -831,7 +831,7 @@ impl Server {
|
|
|
831
831
|
};
|
|
832
832
|
let output = Path::new(&request.output);
|
|
833
833
|
if !output.is_absolute()
|
|
834
|
-
|| request.output.ends_with(
|
|
834
|
+
|| request.output.ends_with(std::path::is_separator)
|
|
835
835
|
|| request.output.len() > MAX_PATH_BYTES
|
|
836
836
|
{
|
|
837
837
|
let message = format!(
|
|
@@ -841,6 +841,12 @@ impl Server {
|
|
|
841
841
|
.replies
|
|
842
842
|
.send(&Reply::Leftovers(reply("invalid", Some(message))));
|
|
843
843
|
}
|
|
844
|
+
#[cfg(windows)]
|
|
845
|
+
if let Some(message) = windows_name_problem(output) {
|
|
846
|
+
return self
|
|
847
|
+
.replies
|
|
848
|
+
.send(&Reply::Leftovers(reply("invalid", Some(message))));
|
|
849
|
+
}
|
|
844
850
|
if let Some(user) = self.output_user(output) {
|
|
845
851
|
return self.replies.send(&Reply::Leftovers(reply(
|
|
846
852
|
"in_use",
|
|
@@ -1227,18 +1233,53 @@ fn in_range<T: PartialOrd + std::fmt::Display>(
|
|
|
1227
1233
|
|
|
1228
1234
|
fn check_output(output: &str) -> Result<PathBuf, String> {
|
|
1229
1235
|
let path = Path::new(output);
|
|
1230
|
-
if !path.is_absolute()
|
|
1236
|
+
if !path.is_absolute()
|
|
1237
|
+
|| output.ends_with(std::path::is_separator)
|
|
1238
|
+
|| output.len() > MAX_PATH_BYTES
|
|
1239
|
+
{
|
|
1231
1240
|
return Err(format!(
|
|
1232
1241
|
"the output must be an absolute file path without its extension, at most {MAX_PATH_BYTES} bytes; it is {}",
|
|
1233
1242
|
output.chars().take(200).collect::<String>()
|
|
1234
1243
|
));
|
|
1235
1244
|
}
|
|
1245
|
+
#[cfg(windows)]
|
|
1246
|
+
if let Some(message) = windows_name_problem(path) {
|
|
1247
|
+
return Err(message);
|
|
1248
|
+
}
|
|
1236
1249
|
match path.parent() {
|
|
1237
1250
|
Some(folder) if folder.is_dir() => Ok(path.to_path_buf()),
|
|
1238
1251
|
_ => Err(format!("the output's folder does not exist: {output}")),
|
|
1239
1252
|
}
|
|
1240
1253
|
}
|
|
1241
1254
|
|
|
1255
|
+
// Why Windows would not take the output's file name as a file of its own: a character it refuses, among them `:`, which
|
|
1256
|
+
// names a stream inside another file, or a device name such as `con` or `nul`, with any extension.
|
|
1257
|
+
#[cfg(windows)]
|
|
1258
|
+
fn windows_name_problem(output: &Path) -> Option<String> {
|
|
1259
|
+
let name = output.file_name()?.to_string_lossy();
|
|
1260
|
+
if name
|
|
1261
|
+
.chars()
|
|
1262
|
+
.any(|character| character < ' ' || "<>:\"|?*".contains(character))
|
|
1263
|
+
{
|
|
1264
|
+
return Some(format!(
|
|
1265
|
+
"the output's file name {name} holds a character Windows refuses in a file name"
|
|
1266
|
+
));
|
|
1267
|
+
}
|
|
1268
|
+
let stem = name
|
|
1269
|
+
.split('.')
|
|
1270
|
+
.next()
|
|
1271
|
+
.unwrap_or_default()
|
|
1272
|
+
.trim_end_matches(' ');
|
|
1273
|
+
let upper = stem.to_ascii_uppercase();
|
|
1274
|
+
let numbered = |prefix: &str| {
|
|
1275
|
+
upper.strip_prefix(prefix).is_some_and(|number| {
|
|
1276
|
+
number.chars().count() == 1 && "0123456789\u{b9}\u{b2}\u{b3}".contains(number)
|
|
1277
|
+
})
|
|
1278
|
+
};
|
|
1279
|
+
(["CON", "PRN", "AUX", "NUL"].contains(&upper.as_str()) || numbered("COM") || numbered("LPT"))
|
|
1280
|
+
.then(|| format!("the output's file name {name} is a device name on Windows"))
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1242
1283
|
// The encoder's answer to what it can do, asked once in the background as the process starts. Nothing waits for it
|
|
1243
1284
|
// but a shutdown, which waits for it to end so no encoder of it is left running; the loop polls it.
|
|
1244
1285
|
struct Probe {
|
|
@@ -1555,4 +1596,33 @@ mod tests {
|
|
|
1555
1596
|
}
|
|
1556
1597
|
);
|
|
1557
1598
|
}
|
|
1599
|
+
|
|
1600
|
+
#[cfg(windows)]
|
|
1601
|
+
#[test]
|
|
1602
|
+
fn output_names_windows_refuses_are_refused_by_name() {
|
|
1603
|
+
let folder = std::env::temp_dir();
|
|
1604
|
+
for name in [
|
|
1605
|
+
"con",
|
|
1606
|
+
"NUL",
|
|
1607
|
+
"com1",
|
|
1608
|
+
"Lpt9",
|
|
1609
|
+
"aux.recording",
|
|
1610
|
+
"prn ",
|
|
1611
|
+
"recording:stream",
|
|
1612
|
+
"a?b",
|
|
1613
|
+
"a|b",
|
|
1614
|
+
] {
|
|
1615
|
+
let refused = check_output(&folder.join(name).to_string_lossy()).expect_err(name);
|
|
1616
|
+
assert!(refused.contains("Windows"), "{refused}");
|
|
1617
|
+
}
|
|
1618
|
+
for name in ["recording-1", "console", "com10", "nullable", "lpt"] {
|
|
1619
|
+
let output = folder.join(name);
|
|
1620
|
+
assert!(check_output(&output.to_string_lossy()).is_ok(), "{name}");
|
|
1621
|
+
}
|
|
1622
|
+
let folder_named = format!("{}\\", folder.join("recording").display());
|
|
1623
|
+
assert!(
|
|
1624
|
+
check_output(&folder_named).is_err(),
|
|
1625
|
+
"a trailing backslash names a folder"
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1558
1628
|
}
|
package/media/src/store.rs
CHANGED
|
@@ -5,9 +5,14 @@
|
|
|
5
5
|
//! reader never sees a place whose bytes are not there yet. The file is the process's own working file: it is
|
|
6
6
|
//! removed when the recording is released, when the process shuts down, or, after the process was killed, by the
|
|
7
7
|
//! client or by a `leftovers` request.
|
|
8
|
+
//!
|
|
9
|
+
//! A reader reads at an offset and never moves the writer's place. On Unix a cloned handle reads with `pread`; on
|
|
10
|
+
//! Windows a cloned handle would share the writer's file pointer, which a positioned read moves, so a reader has a
|
|
11
|
+
//! handle of its own on the same file.
|
|
8
12
|
|
|
9
13
|
use std::fs::{self, File, OpenOptions};
|
|
10
14
|
use std::io::{self, Write};
|
|
15
|
+
#[cfg(unix)]
|
|
11
16
|
use std::os::unix::fs::FileExt;
|
|
12
17
|
use std::path::{Path, PathBuf};
|
|
13
18
|
use std::sync::Mutex;
|
|
@@ -117,7 +122,7 @@ impl FrameStore {
|
|
|
117
122
|
.as_ref()
|
|
118
123
|
.ok_or_else(|| io::Error::from(io::ErrorKind::NotFound))?;
|
|
119
124
|
Ok(StoreReader {
|
|
120
|
-
file: file
|
|
125
|
+
file: reading_handle(file)?,
|
|
121
126
|
})
|
|
122
127
|
}
|
|
123
128
|
|
|
@@ -146,11 +151,62 @@ pub struct StoreReader {
|
|
|
146
151
|
impl StoreReader {
|
|
147
152
|
pub fn read(&self, offset: u64, length: u32) -> io::Result<Vec<u8>> {
|
|
148
153
|
let mut bytes = vec![0u8; length as usize];
|
|
149
|
-
self.file
|
|
154
|
+
read_exact_at(&self.file, &mut bytes, offset)?;
|
|
150
155
|
Ok(bytes)
|
|
151
156
|
}
|
|
152
157
|
}
|
|
153
158
|
|
|
159
|
+
#[cfg(unix)]
|
|
160
|
+
fn reading_handle(file: &File) -> io::Result<File> {
|
|
161
|
+
file.try_clone()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#[cfg(unix)]
|
|
165
|
+
fn read_exact_at(file: &File, bytes: &mut [u8], offset: u64) -> io::Result<()> {
|
|
166
|
+
file.read_exact_at(bytes, offset)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ReOpenFile opens the very file the handle names, not its path, with a file pointer of its own. The writer's handle
|
|
170
|
+
// was opened sharing reads, writes and removal, so this one can share them too.
|
|
171
|
+
#[cfg(windows)]
|
|
172
|
+
fn reading_handle(file: &File) -> io::Result<File> {
|
|
173
|
+
use std::os::windows::io::{AsRawHandle, FromRawHandle};
|
|
174
|
+
use windows_sys::Win32::Storage::FileSystem::{
|
|
175
|
+
FILE_GENERIC_READ, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE, ReOpenFile,
|
|
176
|
+
};
|
|
177
|
+
// SAFETY: the handle is the writer's live file handle; ReOpenFile returns a new handle or INVALID_HANDLE_VALUE.
|
|
178
|
+
let handle = unsafe {
|
|
179
|
+
ReOpenFile(
|
|
180
|
+
file.as_raw_handle(),
|
|
181
|
+
FILE_GENERIC_READ,
|
|
182
|
+
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
183
|
+
0,
|
|
184
|
+
)
|
|
185
|
+
};
|
|
186
|
+
if handle == windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE {
|
|
187
|
+
return Err(io::Error::last_os_error());
|
|
188
|
+
}
|
|
189
|
+
// SAFETY: the new handle is valid and owned by nothing else.
|
|
190
|
+
Ok(unsafe { File::from_raw_handle(handle) })
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[cfg(windows)]
|
|
194
|
+
fn read_exact_at(file: &File, mut bytes: &mut [u8], mut offset: u64) -> io::Result<()> {
|
|
195
|
+
use std::os::windows::fs::FileExt;
|
|
196
|
+
while !bytes.is_empty() {
|
|
197
|
+
match file.seek_read(bytes, offset) {
|
|
198
|
+
Ok(0) => return Err(io::Error::from(io::ErrorKind::UnexpectedEof)),
|
|
199
|
+
Ok(read) => {
|
|
200
|
+
bytes = &mut bytes[read..];
|
|
201
|
+
offset += read as u64;
|
|
202
|
+
}
|
|
203
|
+
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
|
|
204
|
+
Err(error) => return Err(error),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
Ok(())
|
|
208
|
+
}
|
|
209
|
+
|
|
154
210
|
#[cfg(test)]
|
|
155
211
|
mod tests {
|
|
156
212
|
use super::*;
|
|
@@ -176,6 +232,19 @@ mod tests {
|
|
|
176
232
|
assert!(!store.path().exists());
|
|
177
233
|
}
|
|
178
234
|
|
|
235
|
+
#[test]
|
|
236
|
+
fn reading_frames_back_never_moves_where_the_next_frame_is_written() {
|
|
237
|
+
let store = FrameStore::create(path("interleaved"), 1024).expect("created");
|
|
238
|
+
assert_eq!(store.append(b"first"), Ok(0));
|
|
239
|
+
let reader = store.reader().expect("opens");
|
|
240
|
+
assert_eq!(reader.read(0, 2).expect("reads"), b"fi");
|
|
241
|
+
assert_eq!(store.append(b"second"), Ok(5));
|
|
242
|
+
assert_eq!(reader.read(1, 3).expect("reads"), b"irs");
|
|
243
|
+
assert_eq!(store.append(b"third"), Ok(11));
|
|
244
|
+
assert_eq!(reader.read(0, 16).expect("reads"), b"firstsecondthird");
|
|
245
|
+
assert!(store.remove());
|
|
246
|
+
}
|
|
247
|
+
|
|
179
248
|
#[test]
|
|
180
249
|
fn a_reader_reads_the_store_s_own_file_even_after_its_path_names_another() {
|
|
181
250
|
let target = path("reopen");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rehearsal-labs/retest",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "One test across web, mobile and desktop. Built for software engineers and coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
"media/Cargo.lock",
|
|
58
58
|
"media/build.rs",
|
|
59
59
|
"media/src/*.rs",
|
|
60
|
+
"supervisor/Cargo.toml",
|
|
61
|
+
"supervisor/Cargo.lock",
|
|
62
|
+
"supervisor/build.rs",
|
|
63
|
+
"supervisor/src/*.rs",
|
|
64
|
+
"supervisor/NOTICES",
|
|
60
65
|
"LICENSE",
|
|
61
66
|
"src/cli/install/media-notices.txt",
|
|
62
67
|
"src/cli/install/licences/**"
|
|
@@ -66,9 +71,9 @@
|
|
|
66
71
|
"provenance": true
|
|
67
72
|
},
|
|
68
73
|
"scripts": {
|
|
69
|
-
"build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && node_modules/typescript/bin/tsc -p tsconfig.build.json && node scripts/write-schemas.ts",
|
|
70
|
-
"typecheck": "node_modules/typescript/bin/tsc -p tsconfig.json && node_modules/typescript-7/bin/tsc -p tsconfig.json && node_modules/typescript/bin/tsc -p examples/tasks/tsconfig.json",
|
|
71
|
-
"typecheck:proofs": "node_modules/typescript/bin/tsc -p proofs/firefox/tsconfig.json && node_modules/typescript-7/bin/tsc -p proofs/firefox/tsconfig.json && node_modules/typescript/bin/tsc -p proofs/webkit/tsconfig.json && node_modules/typescript-7/bin/tsc -p proofs/webkit/tsconfig.json && node_modules/typescript/bin/tsc -p proofs/native/tsconfig.json && node_modules/typescript-7/bin/tsc -p proofs/native/tsconfig.json && node_modules/typescript/bin/tsc -p proofs/media/tsconfig.json && node_modules/typescript-7/bin/tsc -p proofs/media/tsconfig.json",
|
|
74
|
+
"build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && node node_modules/typescript/bin/tsc -p tsconfig.build.json && node scripts/write-schemas.ts",
|
|
75
|
+
"typecheck": "node node_modules/typescript/bin/tsc -p tsconfig.json && node node_modules/typescript-7/bin/tsc -p tsconfig.json && node node_modules/typescript/bin/tsc -p examples/tasks/tsconfig.json",
|
|
76
|
+
"typecheck:proofs": "node node_modules/typescript/bin/tsc -p proofs/firefox/tsconfig.json && node node_modules/typescript-7/bin/tsc -p proofs/firefox/tsconfig.json && node node_modules/typescript/bin/tsc -p proofs/webkit/tsconfig.json && node node_modules/typescript-7/bin/tsc -p proofs/webkit/tsconfig.json && node node_modules/typescript/bin/tsc -p proofs/native/tsconfig.json && node node_modules/typescript-7/bin/tsc -p proofs/native/tsconfig.json && node node_modules/typescript/bin/tsc -p proofs/media/tsconfig.json && node node_modules/typescript-7/bin/tsc -p proofs/media/tsconfig.json",
|
|
72
77
|
"test:unit": "node --conditions=retest-source --test \"tests/unit/**/*.test.ts\"",
|
|
73
78
|
"test:types": "node tests/types/run.ts",
|
|
74
79
|
"test:integration": "node --conditions=retest-source --test --test-concurrency=1 \"tests/integration/**/*.test.ts\"",
|