@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
@@ -0,0 +1,71 @@
1
+ //! `retest-supervisor`: how Retest keeps its promise on Windows that nothing it starts outlives a run.
2
+ //!
3
+ //! Windows has no process groups to signal and no `ps` to read, so Retest holds what it starts in job objects, which
4
+ //! only Win32 can make. Each invocation does one thing and prints one JSON line on standard output: `hold` puts the
5
+ //! runner in a job that ends every process in it once the runner's handle closes, as it does when the runner ends,
6
+ //! however it ends; `adopt` puts one launched child, and anything it already started, in a job of its own; `members`,
7
+ //! `process`, `end`, `terminate` and `release` read and end those jobs; `strays` names what the runner's job holds that no
8
+ //! launch's job does; `interrupt` sends Ctrl+C or Ctrl+Break to a launch's console. The runner holds every job's handle itself, so no
9
+ //! supervisor process needs to stay alive. The command line is in `command.rs` and the replies in `reply.rs`.
10
+ //!
11
+ //! On macOS and Linux the program builds and refuses to run: Retest holds its processes in process groups there.
12
+
13
+ #[cfg_attr(not(windows), allow(dead_code))]
14
+ mod command;
15
+ #[cfg(windows)]
16
+ mod jobs;
17
+ #[cfg_attr(not(windows), allow(dead_code))]
18
+ mod reply;
19
+
20
+ use std::process::ExitCode;
21
+
22
+ #[cfg(windows)]
23
+ const USAGE: &str = "usage: retest-supervisor hold --runner <pid> --job <name>\n retest-supervisor adopt --runner <pid> --within <name> --job <name> --pid <pid>\n retest-supervisor members --job <name>\n retest-supervisor process --pid <pid> [--job <name>]\n retest-supervisor end --runner <pid> --job <name>\n retest-supervisor terminate --runner <pid> --job <name> --pid <pid> --started <ticks>\n retest-supervisor release --runner <pid> --job <name> --handle <value>\n retest-supervisor strays --runner <pid> --job <name> [--launches <name>,<name>...]\n retest-supervisor interrupt --runner <pid> --job <name> [--pid <pid>] [--key c|break]\n retest-supervisor --version";
24
+
25
+ #[cfg(not(windows))]
26
+ fn main() -> ExitCode {
27
+ eprintln!(
28
+ "retest-supervisor runs on Windows only; on macOS and Linux Retest holds its processes in process groups."
29
+ );
30
+ ExitCode::from(2)
31
+ }
32
+
33
+ #[cfg(windows)]
34
+ fn main() -> ExitCode {
35
+ use std::io::Write;
36
+
37
+ let command = match command::parse(std::env::args_os().skip(1)) {
38
+ Ok(command) => command,
39
+ Err(message) => {
40
+ eprintln!("retest-supervisor: {message}\n{USAGE}");
41
+ return ExitCode::from(2);
42
+ }
43
+ };
44
+ let line = match command {
45
+ command::Command::Version => format!("{}\n", version_line()),
46
+ other => jobs::answer(other).line(),
47
+ };
48
+ let mut output = std::io::stdout().lock();
49
+ // A reply that could not be written in full is no answer; the reader sees the exit status, not half a line.
50
+ match output
51
+ .write_all(line.as_bytes())
52
+ .and_then(|()| output.flush())
53
+ {
54
+ Ok(()) => ExitCode::SUCCESS,
55
+ Err(_) => ExitCode::from(2),
56
+ }
57
+ }
58
+
59
+ // `retest-supervisor 0.1.0 (x86_64-pc-windows-msvc, release, revision 1a2b3c4)`.
60
+ #[cfg(windows)]
61
+ fn version_line() -> String {
62
+ let revision = option_env!("RETEST_SUPERVISOR_REVISION")
63
+ .map(|revision| format!(", revision {revision}"))
64
+ .unwrap_or_default();
65
+ format!(
66
+ "retest-supervisor {} ({}, {}{revision})",
67
+ env!("CARGO_PKG_VERSION"),
68
+ env!("RETEST_SUPERVISOR_TARGET"),
69
+ env!("RETEST_SUPERVISOR_PROFILE")
70
+ )
71
+ }
@@ -0,0 +1,367 @@
1
+ //! The one JSON line each invocation prints. Every reply carries `schemaVersion` and `type`; a request Windows or the
2
+ //! supervisor refused is a `refused` reply with the reason, so the reader always has a sentence to show, and, apart from
3
+ //! it, the Windows call that failed and its code, for a diagnostic. Nothing here touches the system, so it is the same,
4
+ //! and tested, on every platform.
5
+
6
+ /// One process as Windows shows it: its id, the id of the process that started it, when it started, as the 100 ns
7
+ /// ticks since 1601 Windows keeps, the executable it runs and its command line. The start is printed as a string, since
8
+ /// it does not fit the integers JSON readers keep exactly.
9
+ #[derive(Debug, Clone, PartialEq, Eq)]
10
+ pub struct Identity {
11
+ pub pid: u32,
12
+ /// Absent when Windows would not say.
13
+ pub parent_pid: Option<u32>,
14
+ pub started_at: u64,
15
+ /// Absent when Windows would not say.
16
+ pub image: Option<String>,
17
+ /// Absent when Windows would not say.
18
+ pub command_line: Option<String>,
19
+ }
20
+
21
+ #[derive(Debug, PartialEq, Eq)]
22
+ pub enum Reply {
23
+ Held {
24
+ job: String,
25
+ runner: u32,
26
+ handle: usize,
27
+ },
28
+ /// `ended` says the launch had already ended, so it was not put in the job, though what it started was. `late` are
29
+ /// the processes it had started that were put in the job with it; `by_parent` those that could not be moved, being
30
+ /// in a job of their own inside the runner's, beside the launch, and that the process that started them holds;
31
+ /// `unheld` those that could not be held at all. `complete` is false when the search for them stopped before it
32
+ /// found no more.
33
+ Adopted {
34
+ job: String,
35
+ handle: usize,
36
+ root: Identity,
37
+ ended: bool,
38
+ late: Vec<Identity>,
39
+ by_parent: Vec<Identity>,
40
+ unheld: Vec<u32>,
41
+ complete: bool,
42
+ },
43
+ Members {
44
+ job: String,
45
+ processes: Vec<Identity>,
46
+ },
47
+ Process {
48
+ process: Option<Identity>,
49
+ in_job: Option<bool>,
50
+ },
51
+ Ended {
52
+ job: String,
53
+ },
54
+ Terminated {
55
+ pid: u32,
56
+ outcome: Outcome,
57
+ },
58
+ Released {
59
+ job: String,
60
+ },
61
+ Strays {
62
+ job: String,
63
+ processes: Vec<Identity>,
64
+ },
65
+ /// The console Ctrl+C was sent to, by the job's process it was reached through, and every process on it then.
66
+ Interrupted {
67
+ job: String,
68
+ console: u32,
69
+ reached: Vec<u32>,
70
+ },
71
+ /// `problem` is a sentence a person can read; `detail` names the Windows call that failed and its code.
72
+ Refused {
73
+ problem: String,
74
+ detail: Option<String>,
75
+ },
76
+ }
77
+
78
+ /// What `terminate` found: the recorded process, ended; no process under the pid; or another process under it.
79
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
80
+ pub enum Outcome {
81
+ Terminated,
82
+ Absent,
83
+ Different,
84
+ }
85
+
86
+ impl Reply {
87
+ /// The reply as one line of JSON, ending in a newline.
88
+ pub fn line(&self) -> String {
89
+ let fields = match self {
90
+ Self::Held {
91
+ job,
92
+ runner,
93
+ handle,
94
+ } => format!(
95
+ "\"type\":\"held\",\"job\":{},\"runner\":{runner},\"handle\":{handle}",
96
+ quote(job)
97
+ ),
98
+ Self::Adopted {
99
+ job,
100
+ handle,
101
+ root,
102
+ ended,
103
+ late,
104
+ by_parent,
105
+ unheld,
106
+ complete,
107
+ } => format!(
108
+ "\"type\":\"adopted\",\"job\":{},\"handle\":{handle},\"root\":{},\"ended\":{ended},\"late\":{},\"byParent\":{},\"unheld\":{},\"complete\":{complete}",
109
+ quote(job),
110
+ identity(root),
111
+ identities(late),
112
+ identities(by_parent),
113
+ numbers(unheld)
114
+ ),
115
+ Self::Members { job, processes } => format!(
116
+ "\"type\":\"members\",\"job\":{},\"processes\":{}",
117
+ quote(job),
118
+ identities(processes)
119
+ ),
120
+ Self::Process { process, in_job } => format!(
121
+ "\"type\":\"process\",\"process\":{},\"inJob\":{}",
122
+ process.as_ref().map_or_else(|| "null".to_owned(), identity),
123
+ in_job.map_or("null", |held| if held { "true" } else { "false" })
124
+ ),
125
+ Self::Ended { job } => format!("\"type\":\"ended\",\"job\":{}", quote(job)),
126
+ Self::Terminated { pid, outcome } => {
127
+ let outcome = match outcome {
128
+ Outcome::Terminated => "terminated",
129
+ Outcome::Absent => "absent",
130
+ Outcome::Different => "different",
131
+ };
132
+ format!("\"type\":\"terminated\",\"pid\":{pid},\"outcome\":\"{outcome}\"")
133
+ }
134
+ Self::Released { job } => format!("\"type\":\"released\",\"job\":{}", quote(job)),
135
+ Self::Strays { job, processes } => format!(
136
+ "\"type\":\"strays\",\"job\":{},\"processes\":{}",
137
+ quote(job),
138
+ identities(processes)
139
+ ),
140
+ Self::Interrupted {
141
+ job,
142
+ console,
143
+ reached,
144
+ } => format!(
145
+ "\"type\":\"interrupted\",\"job\":{},\"console\":{console},\"reached\":{}",
146
+ quote(job),
147
+ numbers(reached)
148
+ ),
149
+ Self::Refused { problem, detail } => format!(
150
+ "\"type\":\"refused\",\"problem\":{},\"detail\":{}",
151
+ quote(problem),
152
+ detail.as_deref().map_or_else(|| "null".to_owned(), quote)
153
+ ),
154
+ };
155
+ format!("{{\"schemaVersion\":1,{fields}}}\n")
156
+ }
157
+ }
158
+
159
+ fn identity(process: &Identity) -> String {
160
+ format!(
161
+ "{{\"pid\":{},\"parentPid\":{},\"startedAt\":\"{}\",\"image\":{},\"commandLine\":{}}}",
162
+ process.pid,
163
+ process
164
+ .parent_pid
165
+ .map_or_else(|| "null".to_owned(), |parent| parent.to_string()),
166
+ process.started_at,
167
+ process
168
+ .image
169
+ .as_deref()
170
+ .map_or_else(|| "null".to_owned(), quote),
171
+ process
172
+ .command_line
173
+ .as_deref()
174
+ .map_or_else(|| "null".to_owned(), quote)
175
+ )
176
+ }
177
+
178
+ fn numbers(values: &[u32]) -> String {
179
+ format!(
180
+ "[{}]",
181
+ values
182
+ .iter()
183
+ .map(u32::to_string)
184
+ .collect::<Vec<String>>()
185
+ .join(",")
186
+ )
187
+ }
188
+
189
+ fn identities(processes: &[Identity]) -> String {
190
+ format!(
191
+ "[{}]",
192
+ processes
193
+ .iter()
194
+ .map(identity)
195
+ .collect::<Vec<String>>()
196
+ .join(",")
197
+ )
198
+ }
199
+
200
+ /// A JSON string: quotes, backslashes and control characters escaped, everything else as it is, so a path with spaces
201
+ /// or letters outside ASCII reads back the same.
202
+ pub fn quote(text: &str) -> String {
203
+ let mut quoted = String::with_capacity(text.len() + 2);
204
+ quoted.push('"');
205
+ for character in text.chars() {
206
+ match character {
207
+ '"' => quoted.push_str("\\\""),
208
+ '\\' => quoted.push_str("\\\\"),
209
+ '\n' => quoted.push_str("\\n"),
210
+ '\r' => quoted.push_str("\\r"),
211
+ '\t' => quoted.push_str("\\t"),
212
+ control
213
+ if (control as u32) < 0x20 || control == '\u{2028}' || control == '\u{2029}' =>
214
+ {
215
+ quoted.push_str(&format!("\\u{:04x}", control as u32));
216
+ }
217
+ other => quoted.push(other),
218
+ }
219
+ }
220
+ quoted.push('"');
221
+ quoted
222
+ }
223
+
224
+ #[cfg(test)]
225
+ mod tests {
226
+ use super::*;
227
+
228
+ fn chrome(pid: u32, parent: Option<u32>) -> Identity {
229
+ Identity {
230
+ pid,
231
+ parent_pid: parent,
232
+ started_at: 133_735_123_456_789_012,
233
+ image: Some("C:\\Program Files\\Google\\Chrome\\chrome.exe".to_owned()),
234
+ command_line: Some("\"chrome.exe\" --user-data-dir=C:\\p".to_owned()),
235
+ }
236
+ }
237
+
238
+ #[test]
239
+ fn every_reply_is_one_line_with_its_schema_version_and_type() {
240
+ let replies = [
241
+ Reply::Held {
242
+ job: "Local\\retest-40-ab".to_owned(),
243
+ runner: 40,
244
+ handle: 1484,
245
+ },
246
+ Reply::Adopted {
247
+ job: "Local\\retest-40-ab-42".to_owned(),
248
+ handle: 1488,
249
+ root: chrome(42, Some(40)),
250
+ ended: false,
251
+ late: vec![chrome(43, Some(42))],
252
+ by_parent: vec![chrome(45, Some(42))],
253
+ unheld: vec![44],
254
+ complete: true,
255
+ },
256
+ Reply::Members {
257
+ job: "Local\\retest-40-ab-42".to_owned(),
258
+ processes: vec![],
259
+ },
260
+ Reply::Process {
261
+ process: None,
262
+ in_job: None,
263
+ },
264
+ Reply::Ended {
265
+ job: "Local\\retest-40-ab-42".to_owned(),
266
+ },
267
+ Reply::Terminated {
268
+ pid: 43,
269
+ outcome: Outcome::Different,
270
+ },
271
+ Reply::Released {
272
+ job: "Local\\retest-40-ab-42".to_owned(),
273
+ },
274
+ Reply::Strays {
275
+ job: "Local\\retest-40-ab".to_owned(),
276
+ processes: vec![chrome(46, Some(41))],
277
+ },
278
+ Reply::Interrupted {
279
+ job: "Local\\retest-40-ab-42".to_owned(),
280
+ console: 42,
281
+ reached: vec![42, 43],
282
+ },
283
+ Reply::Refused {
284
+ problem: "No job named Local\\retest-40-ab-42 is held.".to_owned(),
285
+ detail: None,
286
+ },
287
+ Reply::Refused {
288
+ problem: "Process 42 could not be held in a job of its own.".to_owned(),
289
+ detail: Some("AssignProcessToJobObject failed with Windows error 5.".to_owned()),
290
+ },
291
+ ];
292
+ for reply in replies {
293
+ let line = reply.line();
294
+ assert!(
295
+ line.starts_with("{\"schemaVersion\":1,\"type\":\""),
296
+ "{line}"
297
+ );
298
+ assert!(line.ends_with("}\n"), "{line}");
299
+ assert_eq!(line.matches('\n').count(), 1, "{line}");
300
+ }
301
+ }
302
+
303
+ #[test]
304
+ fn an_identity_keeps_its_start_exactly_as_a_string_and_its_absent_parts_as_null() {
305
+ let line = Reply::Members {
306
+ job: "Local\\retest-1-a-2".to_owned(),
307
+ processes: vec![
308
+ chrome(42, Some(40)),
309
+ Identity {
310
+ pid: 43,
311
+ parent_pid: None,
312
+ started_at: u64::MAX,
313
+ image: None,
314
+ command_line: None,
315
+ },
316
+ ],
317
+ }
318
+ .line();
319
+ assert_eq!(
320
+ line,
321
+ "{\"schemaVersion\":1,\"type\":\"members\",\"job\":\"Local\\\\retest-1-a-2\",\"processes\":[{\"pid\":42,\"parentPid\":40,\"startedAt\":\"133735123456789012\",\"image\":\"C:\\\\Program Files\\\\Google\\\\Chrome\\\\chrome.exe\",\"commandLine\":\"\\\"chrome.exe\\\" --user-data-dir=C:\\\\p\"},{\"pid\":43,\"parentPid\":null,\"startedAt\":\"18446744073709551615\",\"image\":null,\"commandLine\":null}]}\n"
322
+ );
323
+ assert_eq!(
324
+ Reply::Process {
325
+ process: Some(chrome(42, None)),
326
+ in_job: Some(true)
327
+ }
328
+ .line(),
329
+ "{\"schemaVersion\":1,\"type\":\"process\",\"process\":{\"pid\":42,\"parentPid\":null,\"startedAt\":\"133735123456789012\",\"image\":\"C:\\\\Program Files\\\\Google\\\\Chrome\\\\chrome.exe\",\"commandLine\":\"\\\"chrome.exe\\\" --user-data-dir=C:\\\\p\"},\"inJob\":true}\n"
330
+ );
331
+ assert_eq!(
332
+ Reply::Adopted {
333
+ job: "Local\\retest-1-a-2".to_owned(),
334
+ handle: 8,
335
+ root: chrome(2, Some(1)),
336
+ ended: true,
337
+ late: vec![],
338
+ by_parent: vec![],
339
+ unheld: vec![5, 6],
340
+ complete: false
341
+ }
342
+ .line(),
343
+ "{\"schemaVersion\":1,\"type\":\"adopted\",\"job\":\"Local\\\\retest-1-a-2\",\"handle\":8,\"root\":{\"pid\":2,\"parentPid\":1,\"startedAt\":\"133735123456789012\",\"image\":\"C:\\\\Program Files\\\\Google\\\\Chrome\\\\chrome.exe\",\"commandLine\":\"\\\"chrome.exe\\\" --user-data-dir=C:\\\\p\"},\"ended\":true,\"late\":[],\"byParent\":[],\"unheld\":[5,6],\"complete\":false}\n"
344
+ );
345
+ assert_eq!(
346
+ Reply::Refused {
347
+ problem: "Process 42 could not be held in a job of its own.".to_owned(),
348
+ detail: Some("AssignProcessToJobObject failed with Windows error 5.".to_owned())
349
+ }
350
+ .line(),
351
+ "{\"schemaVersion\":1,\"type\":\"refused\",\"problem\":\"Process 42 could not be held in a job of its own.\",\"detail\":\"AssignProcessToJobObject failed with Windows error 5.\"}\n"
352
+ );
353
+ }
354
+
355
+ #[test]
356
+ fn quoting_escapes_what_json_requires_and_keeps_every_other_character() {
357
+ assert_eq!(quote("plain"), "\"plain\"");
358
+ assert_eq!(quote("a \"b\" \\c"), "\"a \\\"b\\\" \\\\c\"");
359
+ assert_eq!(quote("line\nnext\r\tend"), "\"line\\nnext\\r\\tend\"");
360
+ assert_eq!(quote("\u{0}\u{1f}\u{7f}"), "\"\\u0000\\u001f\u{7f}\"");
361
+ assert_eq!(quote("\u{2028}\u{2029}"), "\"\\u2028\\u2029\"");
362
+ assert_eq!(
363
+ quote("C:\\Users\\Zoë\\アプリ\\app.exe"),
364
+ "\"C:\\\\Users\\\\Zoë\\\\アプリ\\\\app.exe\""
365
+ );
366
+ }
367
+ }