@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.
Files changed (218) hide show
  1. package/README.md +6 -5
  2. package/dist/api/source-location.d.ts.map +1 -1
  3. package/dist/api/source-location.js +3 -1
  4. package/dist/browser/builds.d.ts +35 -6
  5. package/dist/browser/builds.d.ts.map +1 -1
  6. package/dist/browser/builds.js +115 -16
  7. package/dist/browser/cdp/transport.d.ts.map +1 -1
  8. package/dist/browser/cdp/transport.js +4 -4
  9. package/dist/browser/chromium-process.d.ts +23 -0
  10. package/dist/browser/chromium-process.d.ts.map +1 -1
  11. package/dist/browser/chromium-process.js +78 -13
  12. package/dist/browser/electron.d.ts +1 -1
  13. package/dist/browser/electron.d.ts.map +1 -1
  14. package/dist/browser/electron.js +6 -3
  15. package/dist/browser/executables.d.ts +16 -1
  16. package/dist/browser/executables.d.ts.map +1 -1
  17. package/dist/browser/executables.js +50 -18
  18. package/dist/browser/firefox/accessible-names.d.ts +5 -4
  19. package/dist/browser/firefox/accessible-names.d.ts.map +1 -1
  20. package/dist/browser/firefox/accessible-names.js +11 -6
  21. package/dist/browser/firefox/bridge.js +3 -2
  22. package/dist/browser/firefox/executable.d.ts +14 -14
  23. package/dist/browser/firefox/executable.d.ts.map +1 -1
  24. package/dist/browser/firefox/executable.js +29 -21
  25. package/dist/browser/firefox/launch.js +1 -1
  26. package/dist/browser/firefox/navigation.d.ts +5 -2
  27. package/dist/browser/firefox/navigation.d.ts.map +1 -1
  28. package/dist/browser/firefox/navigation.js +48 -14
  29. package/dist/browser/firefox/orphans.d.ts.map +1 -1
  30. package/dist/browser/firefox/orphans.js +34 -2
  31. package/dist/browser/firefox/page.d.ts.map +1 -1
  32. package/dist/browser/firefox/page.js +113 -19
  33. package/dist/browser/firefox/process.d.ts +20 -1
  34. package/dist/browser/firefox/process.d.ts.map +1 -1
  35. package/dist/browser/firefox/process.js +124 -22
  36. package/dist/browser/firefox/profile.d.ts +5 -0
  37. package/dist/browser/firefox/profile.d.ts.map +1 -1
  38. package/dist/browser/firefox/profile.js +17 -1
  39. package/dist/browser/firefox/storage.d.ts.map +1 -1
  40. package/dist/browser/firefox/storage.js +4 -2
  41. package/dist/browser/firefox/subscriptions.d.ts +26 -0
  42. package/dist/browser/firefox/subscriptions.d.ts.map +1 -0
  43. package/dist/browser/firefox/subscriptions.js +10 -0
  44. package/dist/browser/launch.d.ts +6 -0
  45. package/dist/browser/launch.d.ts.map +1 -1
  46. package/dist/browser/launch.js +14 -1
  47. package/dist/cli/commands/doctor.js +3 -3
  48. package/dist/cli/commands/inspect.js +3 -2
  49. package/dist/cli/commands/install.d.ts.map +1 -1
  50. package/dist/cli/commands/install.js +26 -13
  51. package/dist/cli/commands/licences.d.ts +22 -0
  52. package/dist/cli/commands/licences.d.ts.map +1 -1
  53. package/dist/cli/commands/licences.js +62 -20
  54. package/dist/cli/commands/run.js +2 -2
  55. package/dist/cli/doctor/checks.js +4 -1
  56. package/dist/cli/inspect/diagnostics.js +3 -3
  57. package/dist/cli/inspect/test-timeline.js +10 -10
  58. package/dist/cli/install/doctor-rows.d.ts.map +1 -1
  59. package/dist/cli/install/doctor-rows.js +5 -4
  60. package/dist/cli/install/file-changes.d.ts +44 -0
  61. package/dist/cli/install/file-changes.d.ts.map +1 -0
  62. package/dist/cli/install/file-changes.js +81 -0
  63. package/dist/cli/install/install-archive.d.ts +3 -1
  64. package/dist/cli/install/install-archive.d.ts.map +1 -1
  65. package/dist/cli/install/install-archive.js +80 -33
  66. package/dist/cli/install/lock.d.ts.map +1 -1
  67. package/dist/cli/install/lock.js +108 -21
  68. package/dist/cli/install/media-doctor.d.ts.map +1 -1
  69. package/dist/cli/install/media-doctor.js +2 -1
  70. package/dist/cli/install/media-install.d.ts.map +1 -1
  71. package/dist/cli/install/media-install.js +38 -19
  72. package/dist/cli/install/media-pins.d.ts +7 -1
  73. package/dist/cli/install/media-pins.d.ts.map +1 -1
  74. package/dist/cli/install/media-pins.js +23 -12
  75. package/dist/cli/install/media-record.d.ts.map +1 -1
  76. package/dist/cli/install/media-record.js +10 -9
  77. package/dist/cli/install/media-report.d.ts +3 -1
  78. package/dist/cli/install/media-report.d.ts.map +1 -1
  79. package/dist/cli/install/media-report.js +7 -5
  80. package/dist/cli/install/media-tools.d.ts.map +1 -1
  81. package/dist/cli/install/media-tools.js +6 -4
  82. package/dist/cli/install/process-start.d.ts +3 -2
  83. package/dist/cli/install/process-start.d.ts.map +1 -1
  84. package/dist/cli/install/process-start.js +4 -0
  85. package/dist/cli/install/report.d.ts.map +1 -1
  86. package/dist/cli/install/report.js +11 -7
  87. package/dist/cli/install/supervisor-install.d.ts +33 -0
  88. package/dist/cli/install/supervisor-install.d.ts.map +1 -0
  89. package/dist/cli/install/supervisor-install.js +158 -0
  90. package/dist/cli/install/supervisor-locate.d.ts +47 -0
  91. package/dist/cli/install/supervisor-locate.d.ts.map +1 -0
  92. package/dist/cli/install/supervisor-locate.js +113 -0
  93. package/dist/cli/install/supervisor-pins.d.ts +21 -0
  94. package/dist/cli/install/supervisor-pins.d.ts.map +1 -0
  95. package/dist/cli/install/supervisor-pins.js +28 -0
  96. package/dist/cli/install/supervisor-record.d.ts +31 -0
  97. package/dist/cli/install/supervisor-record.d.ts.map +1 -0
  98. package/dist/cli/install/supervisor-record.js +104 -0
  99. package/dist/cli/install/supervisor-report.d.ts +11 -0
  100. package/dist/cli/install/supervisor-report.d.ts.map +1 -0
  101. package/dist/cli/install/supervisor-report.js +33 -0
  102. package/dist/cli/install/supervisor-tools.d.ts +43 -0
  103. package/dist/cli/install/supervisor-tools.d.ts.map +1 -0
  104. package/dist/cli/install/supervisor-tools.js +114 -0
  105. package/dist/cli/install/unpack.d.ts +23 -3
  106. package/dist/cli/install/unpack.d.ts.map +1 -1
  107. package/dist/cli/install/unpack.js +124 -10
  108. package/dist/cli/install/zip-member.d.ts +15 -0
  109. package/dist/cli/install/zip-member.d.ts.map +1 -0
  110. package/dist/cli/install/zip-member.js +77 -0
  111. package/dist/cli/interrupt.d.ts +1 -1
  112. package/dist/cli/interrupt.d.ts.map +1 -1
  113. package/dist/cli/interrupt.js +15 -10
  114. package/dist/cli/terminal.d.ts +9 -1
  115. package/dist/cli/terminal.d.ts.map +1 -1
  116. package/dist/cli/terminal.js +14 -0
  117. package/dist/config/define.d.ts +2 -2
  118. package/dist/config/define.js +2 -2
  119. package/dist/diagnostics/firefox-collector.d.ts +11 -10
  120. package/dist/diagnostics/firefox-collector.d.ts.map +1 -1
  121. package/dist/diagnostics/firefox-collector.js +16 -11
  122. package/dist/media/client.d.ts +2 -1
  123. package/dist/media/client.d.ts.map +1 -1
  124. package/dist/media/client.js +16 -5
  125. package/dist/media/host.d.ts +32 -0
  126. package/dist/media/host.d.ts.map +1 -0
  127. package/dist/media/host.js +50 -0
  128. package/dist/media/locate.d.ts.map +1 -1
  129. package/dist/media/locate.js +4 -3
  130. package/dist/native/processes.d.ts.map +1 -1
  131. package/dist/native/processes.js +7 -3
  132. package/dist/protocol/files.d.ts +1 -1
  133. package/dist/protocol/files.d.ts.map +1 -1
  134. package/dist/protocol/files.js +10 -5
  135. package/dist/reporters/commands.d.ts +12 -3
  136. package/dist/reporters/commands.d.ts.map +1 -1
  137. package/dist/reporters/commands.js +29 -11
  138. package/dist/reporters/human-card.js +1 -1
  139. package/dist/reporters/human.d.ts +3 -1
  140. package/dist/reporters/human.d.ts.map +1 -1
  141. package/dist/reporters/human.js +8 -8
  142. package/dist/reporters/style.d.ts +21 -2
  143. package/dist/reporters/style.d.ts.map +1 -1
  144. package/dist/reporters/style.js +11 -2
  145. package/dist/runner/app-server.d.ts +27 -7
  146. package/dist/runner/app-server.d.ts.map +1 -1
  147. package/dist/runner/app-server.js +74 -24
  148. package/dist/runner/child.js +6 -2
  149. package/dist/runner/media-leftovers.d.ts +7 -3
  150. package/dist/runner/media-leftovers.d.ts.map +1 -1
  151. package/dist/runner/media-leftovers.js +27 -6
  152. package/dist/runner/resources.d.ts +8 -0
  153. package/dist/runner/resources.d.ts.map +1 -1
  154. package/dist/runner/resources.js +12 -1
  155. package/dist/runner/run-session.d.ts.map +1 -1
  156. package/dist/runner/run-session.js +9 -4
  157. package/dist/runner/run.d.ts.map +1 -1
  158. package/dist/runner/run.js +2 -1
  159. package/dist/runner/target-drivers.d.ts +5 -2
  160. package/dist/runner/target-drivers.d.ts.map +1 -1
  161. package/dist/runner/target-drivers.js +10 -1
  162. package/dist/runner/test-file-process.d.ts +8 -0
  163. package/dist/runner/test-file-process.d.ts.map +1 -1
  164. package/dist/runner/test-file-process.js +26 -3
  165. package/dist/runner/test-pages.d.ts.map +1 -1
  166. package/dist/runner/test-pages.js +5 -2
  167. package/dist/shared/environment.d.ts +17 -0
  168. package/dist/shared/environment.d.ts.map +1 -0
  169. package/dist/shared/environment.js +27 -0
  170. package/dist/shared/metadata-process-worker.js +15 -7
  171. package/dist/shared/metadata-process.d.ts +5 -0
  172. package/dist/shared/metadata-process.d.ts.map +1 -1
  173. package/dist/shared/metadata-process.js +36 -6
  174. package/dist/shared/posix-path.d.ts +12 -0
  175. package/dist/shared/posix-path.d.ts.map +1 -1
  176. package/dist/shared/posix-path.js +16 -0
  177. package/dist/shared/private-file.d.ts +38 -0
  178. package/dist/shared/private-file.d.ts.map +1 -0
  179. package/dist/shared/private-file.js +132 -0
  180. package/dist/shared/process-ownership.d.ts +52 -2
  181. package/dist/shared/process-ownership.d.ts.map +1 -1
  182. package/dist/shared/process-ownership.js +104 -11
  183. package/dist/shared/process-supervisor.d.ts +309 -0
  184. package/dist/shared/process-supervisor.d.ts.map +1 -0
  185. package/dist/shared/process-supervisor.js +737 -0
  186. package/dist/shared/process-system.d.ts +113 -0
  187. package/dist/shared/process-system.d.ts.map +1 -0
  188. package/dist/shared/process-system.js +254 -0
  189. package/dist/shared/regular-file.d.ts +2 -2
  190. package/dist/shared/regular-file.d.ts.map +1 -1
  191. package/dist/shared/regular-file.js +19 -2
  192. package/dist/store/artifacts.d.ts +5 -4
  193. package/dist/store/artifacts.d.ts.map +1 -1
  194. package/dist/store/artifacts.js +18 -5
  195. package/dist/store/run-store.d.ts +5 -2
  196. package/dist/store/run-store.d.ts.map +1 -1
  197. package/dist/store/run-store.js +48 -4
  198. package/media/Cargo.lock +16 -0
  199. package/media/Cargo.toml +16 -1
  200. package/media/src/encoder.rs +56 -12
  201. package/media/src/frame.rs +7 -1
  202. package/media/src/main.rs +20 -2
  203. package/media/src/place.rs +230 -11
  204. package/media/src/process_ownership_windows.rs +1058 -0
  205. package/media/src/recording.rs +15 -7
  206. package/media/src/replies.rs +55 -0
  207. package/media/src/server.rs +72 -2
  208. package/media/src/store.rs +71 -2
  209. package/package.json +9 -4
  210. package/src/cli/install/media-notices.txt +465 -1
  211. package/supervisor/Cargo.lock +25 -0
  212. package/supervisor/Cargo.toml +22 -0
  213. package/supervisor/NOTICES +464 -0
  214. package/supervisor/build.rs +44 -0
  215. package/supervisor/src/command.rs +612 -0
  216. package/supervisor/src/jobs.rs +1650 -0
  217. package/supervisor/src/main.rs +71 -0
  218. package/supervisor/src/reply.rs +367 -0
@@ -307,13 +307,14 @@ impl Recording {
307
307
  plan.fps,
308
308
  &plan.partial_path(),
309
309
  );
310
- let mut child = encoder::spawn_in_group(ffmpeg, &arguments, Stdio::piped(), Stdio::null())
311
- .map_err(|error| EncoderFailure {
312
- message: format!("{} could not be started: {error}", ffmpeg.display()),
313
- exit: None,
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
  {
@@ -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();
@@ -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() || output.ends_with('/') || output.len() > MAX_PATH_BYTES {
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
  }
@@ -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.try_clone()?,
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.read_exact_at(&mut bytes, offset)?;
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.2.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\"",