@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,612 @@
1
+ //! The command line: one verb and its named values, each given once. Nothing here touches the system, so it is the
2
+ //! same, and tested, on every platform.
3
+
4
+ use std::ffi::OsString;
5
+
6
+ /// What one invocation is asked to do.
7
+ #[derive(Debug, PartialEq, Eq)]
8
+ pub enum Command {
9
+ Version,
10
+ /// Puts the runner, the process that started this one, in a new job that ends everything in it once the last
11
+ /// handle to it closes, and leaves that handle with the runner.
12
+ Hold {
13
+ runner: u32,
14
+ job: JobName,
15
+ },
16
+ /// Puts one child of the runner in a new job of its own, with the processes it already started, and leaves the
17
+ /// job's handle with the runner. `within` is the runner's own job, which every process the child started is in.
18
+ Adopt {
19
+ runner: u32,
20
+ within: JobName,
21
+ job: JobName,
22
+ pid: u32,
23
+ },
24
+ /// The processes the job holds now.
25
+ Members {
26
+ job: JobName,
27
+ },
28
+ /// One process as it is now, and whether the job holds it.
29
+ Process {
30
+ pid: u32,
31
+ job: Option<JobName>,
32
+ },
33
+ /// Ends every process the job holds, unless it holds the runner.
34
+ End {
35
+ runner: u32,
36
+ job: JobName,
37
+ },
38
+ /// Ends one process of the job, only while it is the process that started at `started`.
39
+ Terminate {
40
+ runner: u32,
41
+ job: JobName,
42
+ pid: u32,
43
+ started: u64,
44
+ },
45
+ /// Closes the runner's handle to an empty job.
46
+ Release {
47
+ runner: u32,
48
+ job: JobName,
49
+ handle: usize,
50
+ },
51
+ /// The processes in the runner's job that no launch's job holds and that nothing still running in it stands
52
+ /// behind: what a launch left when the process that started it had already ended.
53
+ Strays {
54
+ runner: u32,
55
+ job: JobName,
56
+ launches: Vec<JobName>,
57
+ },
58
+ /// Sends Ctrl+C, or Ctrl+Break, to the console of the job's processes, or of one of them, as a person in that
59
+ /// console would, unless Retest itself is on that console.
60
+ Interrupt {
61
+ runner: u32,
62
+ job: JobName,
63
+ pid: Option<u32>,
64
+ key: Key,
65
+ },
66
+ }
67
+
68
+ /// The key `interrupt` sends: Ctrl+C, which a console program can be set to ignore, or Ctrl+Break, which it cannot.
69
+ #[derive(Debug, PartialEq, Eq, Clone, Copy)]
70
+ pub enum Key {
71
+ CtrlC,
72
+ CtrlBreak,
73
+ }
74
+
75
+ /// A job's name in this session's namespace, as Retest makes them: `Local\retest-` and then letters, digits and dashes.
76
+ #[derive(Debug, PartialEq, Eq, Clone)]
77
+ pub struct JobName(String);
78
+
79
+ const JOB_PREFIX: &str = "Local\\retest-";
80
+ const JOB_NAME_LIMIT: usize = 160;
81
+
82
+ impl JobName {
83
+ /// The name, or why it is not one Retest makes.
84
+ pub fn parse(text: &str) -> Result<Self, String> {
85
+ let rest = text
86
+ .strip_prefix(JOB_PREFIX)
87
+ .ok_or_else(|| format!("a job name starts with {JOB_PREFIX}"))?;
88
+ if rest.is_empty() || text.len() > JOB_NAME_LIMIT {
89
+ return Err(format!(
90
+ "a job name is {JOB_PREFIX} and 1 to {} more characters",
91
+ JOB_NAME_LIMIT - JOB_PREFIX.len()
92
+ ));
93
+ }
94
+ if !rest
95
+ .bytes()
96
+ .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-')
97
+ {
98
+ return Err(
99
+ "a job name holds only letters, digits and dashes after its prefix".to_owned(),
100
+ );
101
+ }
102
+ Ok(Self(text.to_owned()))
103
+ }
104
+
105
+ pub fn as_str(&self) -> &str {
106
+ &self.0
107
+ }
108
+ }
109
+
110
+ /// Reads the arguments after the program's name.
111
+ pub fn parse(arguments: impl Iterator<Item = OsString>) -> Result<Command, String> {
112
+ let words = arguments
113
+ .map(|argument| {
114
+ argument
115
+ .into_string()
116
+ .map_err(|_| "arguments must be valid Unicode".to_owned())
117
+ })
118
+ .collect::<Result<Vec<String>, String>>()?;
119
+ let (verb, rest) = words.split_first().ok_or("name what to do")?;
120
+ if verb == "--version" {
121
+ return if rest.is_empty() {
122
+ Ok(Command::Version)
123
+ } else {
124
+ Err("--version takes nothing after it".to_owned())
125
+ };
126
+ }
127
+ let mut values = Values::read(rest)?;
128
+ let command = match verb.as_str() {
129
+ "hold" => Command::Hold {
130
+ runner: values.pid("runner")?,
131
+ job: values.job("job")?,
132
+ },
133
+ "adopt" => Command::Adopt {
134
+ runner: values.pid("runner")?,
135
+ within: values.job("within")?,
136
+ job: values.job("job")?,
137
+ pid: values.pid("pid")?,
138
+ },
139
+ "members" => Command::Members {
140
+ job: values.job("job")?,
141
+ },
142
+ "process" => Command::Process {
143
+ pid: values.pid("pid")?,
144
+ job: values.optional_job("job")?,
145
+ },
146
+ "end" => Command::End {
147
+ runner: values.pid("runner")?,
148
+ job: values.job("job")?,
149
+ },
150
+ "terminate" => Command::Terminate {
151
+ runner: values.pid("runner")?,
152
+ job: values.job("job")?,
153
+ pid: values.pid("pid")?,
154
+ started: values.number("started")?,
155
+ },
156
+ "release" => Command::Release {
157
+ runner: values.pid("runner")?,
158
+ job: values.job("job")?,
159
+ handle: values.handle("handle")?,
160
+ },
161
+ "strays" => Command::Strays {
162
+ runner: values.pid("runner")?,
163
+ job: values.job("job")?,
164
+ launches: values.jobs("launches")?,
165
+ },
166
+ "interrupt" => Command::Interrupt {
167
+ runner: values.pid("runner")?,
168
+ job: values.job("job")?,
169
+ pid: values.optional_pid("pid")?,
170
+ key: values.key("key")?,
171
+ },
172
+ other => return Err(format!("unknown command {other}")),
173
+ };
174
+ values.finish()?;
175
+ Ok(command)
176
+ }
177
+
178
+ // The `--name value` pairs after the verb, taken out one by one, so any left over is named.
179
+ struct Values(Vec<(String, String)>);
180
+
181
+ impl Values {
182
+ fn read(words: &[String]) -> Result<Self, String> {
183
+ let mut pairs: Vec<(String, String)> = Vec::new();
184
+ let mut words = words.iter();
185
+ while let Some(word) = words.next() {
186
+ let name = word
187
+ .strip_prefix("--")
188
+ .ok_or_else(|| format!("expected a --name before {word}"))?;
189
+ let value = words
190
+ .next()
191
+ .ok_or_else(|| format!("--{name} needs a value"))?;
192
+ if pairs.iter().any(|(known, _)| known == name) {
193
+ return Err(format!("--{name} is given twice"));
194
+ }
195
+ pairs.push((name.to_owned(), value.clone()));
196
+ }
197
+ Ok(Self(pairs))
198
+ }
199
+
200
+ fn take(&mut self, name: &str) -> Option<String> {
201
+ let index = self.0.iter().position(|(known, _)| known == name)?;
202
+ Some(self.0.remove(index).1)
203
+ }
204
+
205
+ fn required(&mut self, name: &str) -> Result<String, String> {
206
+ self.take(name)
207
+ .ok_or_else(|| format!("--{name} is missing"))
208
+ }
209
+
210
+ fn number(&mut self, name: &str) -> Result<u64, String> {
211
+ let text = self.required(name)?;
212
+ whole_number(&text).ok_or_else(|| format!("--{name} takes a whole number, not {text}"))
213
+ }
214
+
215
+ // Pid 0 is the idle process, which nothing Retest starts can be.
216
+ fn pid(&mut self, name: &str) -> Result<u32, String> {
217
+ let number = self.number(name)?;
218
+ u32::try_from(number)
219
+ .ok()
220
+ .filter(|pid| *pid > 0)
221
+ .ok_or_else(|| format!("--{name} takes a process id from 1 to {}", u32::MAX))
222
+ }
223
+
224
+ // Windows hands out handle values in steps of four; anything else names no handle.
225
+ fn handle(&mut self, name: &str) -> Result<usize, String> {
226
+ let number = self.number(name)?;
227
+ usize::try_from(number)
228
+ .ok()
229
+ .filter(|value| *value > 0 && value % 4 == 0)
230
+ .ok_or_else(|| format!("--{name} takes a handle value, a positive multiple of four"))
231
+ }
232
+
233
+ fn job(&mut self, name: &str) -> Result<JobName, String> {
234
+ JobName::parse(&self.required(name)?).map_err(|problem| format!("--{name}: {problem}"))
235
+ }
236
+
237
+ fn optional_job(&mut self, name: &str) -> Result<Option<JobName>, String> {
238
+ self.take(name)
239
+ .map(|text| JobName::parse(&text).map_err(|problem| format!("--{name}: {problem}")))
240
+ .transpose()
241
+ }
242
+
243
+ fn optional_pid(&mut self, name: &str) -> Result<Option<u32>, String> {
244
+ if !self.0.iter().any(|(known, _)| known == name) {
245
+ return Ok(None);
246
+ }
247
+ self.pid(name).map(Some)
248
+ }
249
+
250
+ // `c` or `break`; Ctrl+C when absent.
251
+ fn key(&mut self, name: &str) -> Result<Key, String> {
252
+ match self.take(name).as_deref() {
253
+ None | Some("c") => Ok(Key::CtrlC),
254
+ Some("break") => Ok(Key::CtrlBreak),
255
+ Some(other) => Err(format!("--{name} takes c or break, not {other}")),
256
+ }
257
+ }
258
+
259
+ // Job names joined by commas, which no name holds; absent when there are none.
260
+ fn jobs(&mut self, name: &str) -> Result<Vec<JobName>, String> {
261
+ let Some(text) = self.take(name) else {
262
+ return Ok(Vec::new());
263
+ };
264
+ text.split(',')
265
+ .map(|part| JobName::parse(part).map_err(|problem| format!("--{name}: {problem}")))
266
+ .collect()
267
+ }
268
+
269
+ fn finish(self) -> Result<(), String> {
270
+ match self.0.first() {
271
+ Some((name, _)) => Err(format!("--{name} does not go with this command")),
272
+ None => Ok(()),
273
+ }
274
+ }
275
+ }
276
+
277
+ // Digits only, so a sign, a space or a leading plus never reads as a number.
278
+ fn whole_number(text: &str) -> Option<u64> {
279
+ if text.is_empty() || text.len() > 20 || !text.bytes().all(|byte| byte.is_ascii_digit()) {
280
+ return None;
281
+ }
282
+ text.parse().ok()
283
+ }
284
+
285
+ #[cfg(test)]
286
+ mod tests {
287
+ use super::*;
288
+
289
+ fn read(words: &[&str]) -> Result<Command, String> {
290
+ parse(words.iter().map(OsString::from))
291
+ }
292
+
293
+ fn job(name: &str) -> JobName {
294
+ JobName::parse(name).expect("a valid job name")
295
+ }
296
+
297
+ #[test]
298
+ fn each_command_reads_its_named_values_in_any_order() {
299
+ assert_eq!(read(&["--version"]), Ok(Command::Version));
300
+ assert_eq!(
301
+ read(&["hold", "--job", "Local\\retest-1-ab", "--runner", "1"]),
302
+ Ok(Command::Hold {
303
+ runner: 1,
304
+ job: job("Local\\retest-1-ab")
305
+ })
306
+ );
307
+ assert_eq!(
308
+ read(&[
309
+ "adopt",
310
+ "--runner",
311
+ "40",
312
+ "--pid",
313
+ "42",
314
+ "--job",
315
+ "Local\\retest-40-ab-42-1",
316
+ "--within",
317
+ "Local\\retest-40-ab"
318
+ ]),
319
+ Ok(Command::Adopt {
320
+ runner: 40,
321
+ within: job("Local\\retest-40-ab"),
322
+ job: job("Local\\retest-40-ab-42-1"),
323
+ pid: 42
324
+ })
325
+ );
326
+ assert_eq!(
327
+ read(&["members", "--job", "Local\\retest-40-ab-42"]),
328
+ Ok(Command::Members {
329
+ job: job("Local\\retest-40-ab-42")
330
+ })
331
+ );
332
+ assert_eq!(
333
+ read(&["process", "--pid", "42"]),
334
+ Ok(Command::Process { pid: 42, job: None })
335
+ );
336
+ assert_eq!(
337
+ read(&["process", "--pid", "42", "--job", "Local\\retest-40-ab-42"]),
338
+ Ok(Command::Process {
339
+ pid: 42,
340
+ job: Some(job("Local\\retest-40-ab-42"))
341
+ })
342
+ );
343
+ assert_eq!(
344
+ read(&["end", "--runner", "40", "--job", "Local\\retest-40-ab-42"]),
345
+ Ok(Command::End {
346
+ runner: 40,
347
+ job: job("Local\\retest-40-ab-42")
348
+ })
349
+ );
350
+ assert_eq!(
351
+ read(&[
352
+ "terminate",
353
+ "--runner",
354
+ "40",
355
+ "--job",
356
+ "Local\\retest-40-ab-42",
357
+ "--pid",
358
+ "43",
359
+ "--started",
360
+ "133735123456789012"
361
+ ]),
362
+ Ok(Command::Terminate {
363
+ runner: 40,
364
+ job: job("Local\\retest-40-ab-42"),
365
+ pid: 43,
366
+ started: 133_735_123_456_789_012
367
+ })
368
+ );
369
+ assert_eq!(
370
+ read(&[
371
+ "release",
372
+ "--runner",
373
+ "40",
374
+ "--job",
375
+ "Local\\retest-40-ab-42",
376
+ "--handle",
377
+ "1484"
378
+ ]),
379
+ Ok(Command::Release {
380
+ runner: 40,
381
+ job: job("Local\\retest-40-ab-42"),
382
+ handle: 1484
383
+ })
384
+ );
385
+ assert_eq!(
386
+ read(&[
387
+ "strays",
388
+ "--runner",
389
+ "40",
390
+ "--job",
391
+ "Local\\retest-40-ab",
392
+ "--launches",
393
+ "Local\\retest-40-ab-42-1,Local\\retest-40-ab-43-2"
394
+ ]),
395
+ Ok(Command::Strays {
396
+ runner: 40,
397
+ job: job("Local\\retest-40-ab"),
398
+ launches: vec![
399
+ job("Local\\retest-40-ab-42-1"),
400
+ job("Local\\retest-40-ab-43-2")
401
+ ]
402
+ })
403
+ );
404
+ assert_eq!(
405
+ read(&["strays", "--runner", "40", "--job", "Local\\retest-40-ab"]),
406
+ Ok(Command::Strays {
407
+ runner: 40,
408
+ job: job("Local\\retest-40-ab"),
409
+ launches: vec![]
410
+ })
411
+ );
412
+ assert_eq!(
413
+ read(&[
414
+ "interrupt",
415
+ "--runner",
416
+ "40",
417
+ "--job",
418
+ "Local\\retest-40-ab-42-1"
419
+ ]),
420
+ Ok(Command::Interrupt {
421
+ runner: 40,
422
+ job: job("Local\\retest-40-ab-42-1"),
423
+ pid: None,
424
+ key: Key::CtrlC
425
+ })
426
+ );
427
+ assert_eq!(
428
+ read(&[
429
+ "interrupt",
430
+ "--runner",
431
+ "40",
432
+ "--job",
433
+ "Local\\retest-40-ab-42-1",
434
+ "--pid",
435
+ "42",
436
+ "--key",
437
+ "break"
438
+ ]),
439
+ Ok(Command::Interrupt {
440
+ runner: 40,
441
+ job: job("Local\\retest-40-ab-42-1"),
442
+ pid: Some(42),
443
+ key: Key::CtrlBreak
444
+ })
445
+ );
446
+ assert!(
447
+ read(&[
448
+ "interrupt",
449
+ "--runner",
450
+ "40",
451
+ "--job",
452
+ "Local\\retest-40-ab-42-1",
453
+ "--key",
454
+ "z"
455
+ ])
456
+ .is_err()
457
+ );
458
+ assert!(
459
+ read(&[
460
+ "strays",
461
+ "--runner",
462
+ "40",
463
+ "--job",
464
+ "Local\\retest-40-ab",
465
+ "--launches",
466
+ "Local\\retest-40-ab-42-1,"
467
+ ])
468
+ .is_err(),
469
+ "an empty name in the list is refused"
470
+ );
471
+ assert_eq!(
472
+ read(&[
473
+ "adopt",
474
+ "--runner",
475
+ "40",
476
+ "--pid",
477
+ "42",
478
+ "--job",
479
+ "Local\\retest-40-ab-42-1"
480
+ ]),
481
+ Err("--within is missing".to_owned())
482
+ );
483
+ }
484
+
485
+ #[test]
486
+ fn a_missing_repeated_or_extra_value_is_refused_by_name() {
487
+ assert_eq!(read(&[]), Err("name what to do".to_owned()));
488
+ assert_eq!(
489
+ read(&["hold", "--runner", "1"]),
490
+ Err("--job is missing".to_owned())
491
+ );
492
+ assert_eq!(
493
+ read(&[
494
+ "members",
495
+ "--job",
496
+ "Local\\retest-a",
497
+ "--job",
498
+ "Local\\retest-b"
499
+ ]),
500
+ Err("--job is given twice".to_owned())
501
+ );
502
+ assert_eq!(
503
+ read(&["members", "--job", "Local\\retest-a", "--pid", "4"]),
504
+ Err("--pid does not go with this command".to_owned())
505
+ );
506
+ assert_eq!(
507
+ read(&["members", "--job"]),
508
+ Err("--job needs a value".to_owned())
509
+ );
510
+ assert_eq!(
511
+ read(&["members", "Local\\retest-a"]),
512
+ Err("expected a --name before Local\\retest-a".to_owned())
513
+ );
514
+ assert_eq!(
515
+ read(&["--version", "now"]),
516
+ Err("--version takes nothing after it".to_owned())
517
+ );
518
+ assert_eq!(
519
+ read(&["kill", "--pid", "4"]),
520
+ Err("unknown command kill".to_owned())
521
+ );
522
+ }
523
+
524
+ #[test]
525
+ fn numbers_are_digits_only_and_within_their_range() {
526
+ for text in [
527
+ "",
528
+ "-1",
529
+ "+1",
530
+ " 1",
531
+ "1 ",
532
+ "0x10",
533
+ "1.0",
534
+ "184467440737095516160",
535
+ ] {
536
+ assert!(
537
+ read(&["process", "--pid", text]).is_err(),
538
+ "{text:?} read as a pid"
539
+ );
540
+ }
541
+ assert!(read(&["process", "--pid", "0"]).is_err());
542
+ assert!(read(&["process", "--pid", "4294967296"]).is_err());
543
+ assert_eq!(
544
+ read(&["process", "--pid", "4294967295"]),
545
+ Ok(Command::Process {
546
+ pid: u32::MAX,
547
+ job: None
548
+ })
549
+ );
550
+ assert!(
551
+ read(&[
552
+ "release",
553
+ "--runner",
554
+ "1",
555
+ "--job",
556
+ "Local\\retest-a",
557
+ "--handle",
558
+ "6"
559
+ ])
560
+ .is_err()
561
+ );
562
+ assert!(
563
+ read(&[
564
+ "release",
565
+ "--runner",
566
+ "1",
567
+ "--job",
568
+ "Local\\retest-a",
569
+ "--handle",
570
+ "0"
571
+ ])
572
+ .is_err()
573
+ );
574
+ assert!(
575
+ read(&[
576
+ "terminate",
577
+ "--runner",
578
+ "1",
579
+ "--job",
580
+ "Local\\retest-a",
581
+ "--pid",
582
+ "2",
583
+ "--started",
584
+ "18446744073709551616"
585
+ ])
586
+ .is_err()
587
+ );
588
+ }
589
+
590
+ #[test]
591
+ fn a_job_name_is_one_retest_makes_and_nothing_else() {
592
+ assert!(JobName::parse("Local\\retest-40-0f3c-42").is_ok());
593
+ for name in [
594
+ "",
595
+ "Local\\retest-",
596
+ "Global\\retest-1",
597
+ "retest-1",
598
+ "Local\\retest-1\\x",
599
+ "Local\\retest-1 2",
600
+ "Local\\retest-ä",
601
+ "Local\\Retest-1",
602
+ ] {
603
+ assert!(JobName::parse(name).is_err(), "{name:?} read as a job name");
604
+ }
605
+ let longest = format!(
606
+ "Local\\retest-{}",
607
+ "a".repeat(JOB_NAME_LIMIT - JOB_PREFIX.len())
608
+ );
609
+ assert!(JobName::parse(&longest).is_ok());
610
+ assert!(JobName::parse(&format!("{longest}a")).is_err());
611
+ }
612
+ }