Skip to content
579 linesCodeBlameRaw
1//! What the runner is doing, and when it is not running or is failing, why
2//! and how to fix it: the one catalogue of those words. The tray shows a
3//! diagnosis's title, the settings window all of it with its buttons, and
4//! the site shows it as the app sends it, word for word, on its Runners
5//! page (and keeps it, so a person's other computers are listed with
6//! theirs). The site adds nothing of its own: what it says of a computer
7//! whose g1t is closed is `closed` here, sent with every report.
8//!
9//! Each action is an id the settings window and the site turn into a
10//! button; bridge.rs and settings.js do what it says.
11//!
12//! The sandbox (launcher.rs) says why something is wrong in its own
13//! sentences. Those this catalogue knows (WSL not installed, virtualization
14//! off, Windows Sandbox off, …) are said in its words, with a fix and its
15//! buttons; any other is shown as the launcher wrote it. With the sandbox,
16//! each kind of job it runs (Linux jobs, Windows jobs) has a line of its
17//! own as well (`capabilities`), since one can run while the other cannot.
18
19use serde::Serialize;
20
21use super::launcher::{self, State, Status, Why};
22
23/// Every state the runner can be in.
24#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum Code {
27 /// Running jobs is turned off in settings.
28 #[default]
29 Off,
30 /// The person pressed Stop.
31 Stopped,
32 Starting,
33 /// Waiting for the site to make a registration token.
34 Registering,
35 /// Nobody is signed in to g1t in the app.
36 SignedOut,
37 /// Signed in, owning no workspace: nowhere to register.
38 NoWorkspace,
39 /// The person may not add runners to the workspace.
40 NotAllowed,
41 RegisterFailed,
42 Idle,
43 Busy,
44 NoDocker,
45 /// The `g1t-runner` here is from before its sandbox launcher.
46 NoLauncher,
47 /// Windows, without WSL, which holds the sandbox's Linux VM.
48 NoWsl,
49 /// WSL set to mirrored networking, which reaches this computer's localhost.
50 WslMirrored,
51 /// The computer's virtualization is turned off.
52 NoVirtualization,
53 /// macOS, without vfkit, which runs the sandbox's VM.
54 NoVfkit,
55 /// Linux, with neither Sysbox in Docker nor KVM.
56 NoSysbox,
57 /// The sandbox's image couldn't be downloaded, or isn't the release's.
58 ImageFailed,
59 /// The sandbox can't be made here, for a reason in the launcher's words.
60 SandboxUnavailable,
61 /// The sandbox failed, in the launcher's words.
62 SandboxFailed,
63 /// The computer has a sandbox the app didn't make, and has room for one.
64 SandboxTaken,
65 Updating,
66 UpdateFailed,
67 Offline,
68 Crashed,
69}
70
71/// How a state should look: a dot's colour.
72#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
73#[serde(rename_all = "snake_case")]
74pub enum Tone {
75 Ok,
76 Working,
77 Off,
78 Warning,
79 Problem,
80}
81
82/// A one-click fix.
83#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
84pub struct Action {
85 pub id: &'static str,
86 pub label: &'static str,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
90pub struct Diagnosis {
91 pub code: Code,
92 pub tone: Tone,
93 /// One line: the tray's, and the heading everywhere else.
94 pub title: String,
95 /// Why, in a sentence.
96 pub cause: Option<String>,
97 /// What to do about it.
98 pub fix: Option<String>,
99 pub actions: Vec<Action>,
100 /// With the sandbox: each kind of job it runs, on its own line.
101 #[serde(skip_serializing_if = "Vec::is_empty")]
102 pub capabilities: Vec<CapabilityLine>,
103}
104
105/// One kind of job the sandbox runs, `linux` or `windows` (or `macos`),
106/// and how it is: Linux jobs can run while Windows jobs cannot.
107#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
108pub struct CapabilityLine {
109 pub id: String,
110 /// `Linux jobs`.
111 pub label: String,
112 pub tone: Tone,
113 /// `Waiting for jobs`, `Not available`.
114 pub title: String,
115 pub cause: Option<String>,
116 pub fix: Option<String>,
117 pub actions: Vec<Action>,
118}
119
120/// What the words are filled in with.
121#[derive(Clone, Debug, Default)]
122pub struct Facts {
123 pub workspace: Option<String>,
124 /// How jobs run, as `sandboxed`, `in Docker`, `on this computer`.
125 pub backend: &'static str,
126 pub machine: String,
127 pub job: Option<String>,
128 pub detail: Option<String>,
129 pub exit_code: Option<i32>,
130 pub wait_seconds: Option<u64>,
131 /// The server's host: `g1t.sh`.
132 pub server: String,
133 pub platform: String,
134 /// What the sandbox says to do, for a cause in its own words.
135 pub fix: Option<String>,
136 /// How the sandbox is, when jobs run sandboxed.
137 pub sandbox: Option<Status>,
138}
139
140const START: Action = Action { id: "start", label: "Start" };
141const TURN_ON: Action = Action { id: "start", label: "Turn on" };
142const RETRY: Action = Action { id: "retry", label: "Retry" };
143const RESTART: Action = Action { id: "restart", label: "Restart" };
144const SIGN_IN: Action = Action { id: "sign_in", label: "Sign in" };
145const CHOOSE_WORKSPACE: Action = Action { id: "choose_workspace", label: "Choose workspace" };
146const NEW_WORKSPACE: Action = Action { id: "new_workspace", label: "Create a workspace" };
147const CHOOSE_BACKEND: Action = Action { id: "choose_backend", label: "Choose how jobs run" };
148const OPEN_DOCKER: Action = Action { id: "open_docker", label: "Open Docker" };
149const GET_DOCKER: Action = Action { id: "get_docker", label: "Get Docker" };
150const INSTALL_SYSBOX: Action = Action { id: "install_sysbox", label: "Install Sysbox" };
151const COPY_WSL_INSTALL: Action = Action { id: "copy_wsl_install", label: "Copy command" };
152const COPY_VFKIT_INSTALL: Action = Action { id: "copy_vfkit_install", label: "Copy command" };
153const WINDOWS_FEATURES: Action = Action { id: "windows_features", label: "Open Windows features" };
154const LEARN_MORE: Action = Action { id: "learn_more", label: "Learn more" };
155const VIEW_LOG: Action = Action { id: "view_log", label: "View log" };
156const REPORT: Action = Action { id: "report", label: "Report" };
157const VIEW_RUNNERS: Action = Action { id: "view_runners", label: "View on g1t" };
158
159fn some(text: impl Into<String>) -> Option<String> {
160 Some(text.into())
161}
162
163/// The catalogue.
164pub fn diagnose(code: Code, facts: &Facts) -> Diagnosis {
165 let workspace = facts.workspace.as_deref().unwrap_or("your workspace");
166 let detail = facts.detail.clone();
167 let (tone, title, cause, fix, actions): (Tone, String, Option<String>, Option<String>, Vec<Action>) = match code {
168 Code::Off => (
169 Tone::Off,
170 "Not running: turned off".into(),
171 some("Running jobs on this computer is turned off in g1t's settings."),
172 some("Turn it on to run your workspace's self-hosted jobs here while g1t is open."),
173 vec![TURN_ON],
174 ),
175 Code::Stopped => (
176 Tone::Off,
177 "Not running: stopped".into(),
178 some("You stopped the runner. It starts again when you press Start, or when g1t next opens."),
179 None,
180 vec![START],
181 ),
182 Code::Starting => (Tone::Working, "Starting".into(), detail, None, vec![]),
183 Code::Registering => (
184 Tone::Working,
185 format!("Registering with {workspace}"),
186 some(format!("g1t is registering {} as a runner for {workspace}.", facts.machine)),
187 some("Keep g1t open and signed in; it takes a few seconds."),
188 vec![],
189 ),
190 Code::SignedOut => (
191 Tone::Warning,
192 "Not running: signed out".into(),
193 some("This computer isn't registered yet, and registering it needs you signed in to g1t."),
194 some("Sign in, as an owner of the workspace the runner is for."),
195 vec![SIGN_IN],
196 ),
197 Code::NoWorkspace => (
198 Tone::Warning,
199 "Not running: no workspace to run jobs for".into(),
200 some("Only a workspace's owners add runners to it, and you don't own one."),
201 some("Ask an owner of your workspace to run it, or create a workspace of your own."),
202 vec![NEW_WORKSPACE],
203 ),
204 Code::NotAllowed => (
205 Tone::Problem,
206 format!("Couldn't register with {workspace}"),
207 some(format!("You can't add runners to {workspace}: only its owners can.")),
208 some(format!("Ask an owner of {workspace} to make you one, or choose a workspace you own.")),
209 vec![CHOOSE_WORKSPACE],
210 ),
211 Code::RegisterFailed => (
212 Tone::Problem,
213 format!("Couldn't register with {workspace}"),
214 detail.or_else(|| some("g1t didn't make a registration for this computer.")),
215 some("It tries again by itself in a few minutes."),
216 vec![RETRY, VIEW_LOG],
217 ),
218 Code::Idle => (
219 Tone::Ok,
220 "Waiting for jobs".into(),
221 some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)),
222 None,
223 vec![VIEW_RUNNERS],
224 ),
225 Code::Busy => (
226 Tone::Working,
227 format!("Running {}", facts.job.as_deref().unwrap_or("a job")),
228 some(format!("Running {} for {workspace} on {}.", facts.backend, facts.machine)),
229 None,
230 vec![VIEW_RUNNERS],
231 ),
232 Code::NoDocker => {
233 let app = match facts.platform.as_str() {
234 "windows" | "macos" => "Docker Desktop",
235 _ => "Docker",
236 };
237 (
238 Tone::Problem,
239 "Not running: Docker isn't running".into(),
240 some(format!("Running jobs {} needs Docker, and Docker isn't running on this computer, or isn't installed.", facts.backend)),
241 some(format!("Start {app}, or install it. The runner starts by itself once Docker answers.")),
242 if app == "Docker Desktop" { vec![OPEN_DOCKER, GET_DOCKER] } else { vec![GET_DOCKER] },
243 )
244 }
245 Code::NoLauncher => (
246 Tone::Warning,
247 "Not running: this g1t-runner has no sandbox".into(),
248 some("The g1t-runner g1t downloaded is older than its sandbox launcher, so it can't make a sandbox."),
249 some("g1t looks for a newer one by itself; Retry looks now. Or choose to run jobs in Docker or directly on this computer: neither is a sandbox, and settings say what each can reach."),
250 vec![RETRY, CHOOSE_BACKEND],
251 ),
252 Code::NoWsl => (
253 Tone::Problem,
254 "Not running: WSL isn't installed".into(),
255 some("Linux jobs run in a WSL distribution of the runner's own, and WSL isn't installed on this computer."),
256 some(format!("Run `{}` in an admin terminal, then restart Windows. It works on every edition of Windows 11, Home included.", launcher::WSL_INSTALL)),
257 vec![COPY_WSL_INSTALL, LEARN_MORE],
258 ),
259 Code::WslMirrored => (
260 Tone::Problem,
261 "Not running: WSL uses mirrored networking".into(),
262 some("WSL is set to mirrored networking, in which Linux reaches this computer's own localhost, so the sandbox won't start."),
263 some("Remove networkingMode=mirrored from %USERPROFILE%\\.wslconfig, run `wsl --shutdown`, then Retry."),
264 vec![RETRY, LEARN_MORE],
265 ),
266 Code::NoVirtualization => (
267 Tone::Problem,
268 "Not running: virtualization is off".into(),
269 some("The sandbox is a small virtual machine, and this computer has virtualization turned off."),
270 some(if facts.platform == "windows" {
271 "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, and Virtual Machine Platform in Windows features; then restart."
272 } else {
273 "Turn on virtualization (Intel VT-x or AMD-V) in this computer's firmware settings, then restart."
274 }),
275 if facts.platform == "windows" { vec![WINDOWS_FEATURES, LEARN_MORE] } else { vec![RETRY, LEARN_MORE] },
276 ),
277 Code::NoVfkit => (
278 Tone::Problem,
279 "Not running: vfkit isn't installed".into(),
280 some("Linux jobs run in a small VM that vfkit runs with Apple's Virtualization framework, and vfkit isn't installed."),
281 some(format!("Run `{}` in Terminal (macOS 13 or later). The runner starts by itself once it's there.", launcher::VFKIT_INSTALL)),
282 vec![COPY_VFKIT_INSTALL, LEARN_MORE],
283 ),
284 Code::NoSysbox => (
285 Tone::Problem,
286 "Not running: no Sysbox or KVM".into(),
287 some("The sandbox on Linux is the runner's image under Sysbox, or a VM under QEMU with KVM, and this computer has neither."),
288 some("Install Sysbox 0.7 or later, or turn on virtualization and install QEMU so /dev/kvm exists. The runner starts by itself once one is there."),
289 vec![INSTALL_SYSBOX, CHOOSE_BACKEND],
290 ),
291 Code::ImageFailed => (
292 Tone::Problem,
293 "Not running: the sandbox's image didn't download".into(),
294 detail.or_else(|| some("The sandbox's image couldn't be downloaded, or wasn't the release's, so it wasn't used.")),
295 some("It's only ever used once its signature and SHA-256 are the release's. g1t tries again by itself; check the connection, or report it if it keeps happening."),
296 vec![RETRY, REPORT],
297 ),
298 Code::SandboxUnavailable => (
299 Tone::Problem,
300 "Not running: the sandbox can't run here".into(),
301 detail.or_else(|| some("The sandbox can't be made on this computer.")),
302 facts.fix.clone().or_else(|| some("Choose to run jobs in Docker or directly on this computer instead: neither is a sandbox, and settings say what each can reach.")),
303 vec![RETRY, LEARN_MORE, CHOOSE_BACKEND],
304 ),
305 Code::SandboxFailed => (
306 Tone::Problem,
307 "Not running: the sandbox failed".into(),
308 detail.or_else(|| some("The sandbox stopped, and didn't say why.")),
309 Some(match facts.wait_seconds {
310 Some(wait) => format!("It starts again by itself in {wait} seconds. Retry now, or look at its log."),
311 None => "Retry, or look at its log.".into(),
312 }),
313 vec![RETRY, VIEW_LOG],
314 ),
315 Code::SandboxTaken => (
316 Tone::Warning,
317 "Not running: this computer has another sandbox".into(),
318 some("This computer already has a g1t-runner sandbox that g1t didn't make, such as one started with `g1t-runner sandbox up`, and it has room for one."),
319 some("Remove it with `g1t-runner sandbox down --delete`, then Retry; its runners stay listed on g1t until you remove them there. Or keep it, and turn off running jobs in g1t."),
320 vec![RETRY, LEARN_MORE],
321 ),
322 Code::Updating => (Tone::Working, "Updating".into(), detail, some("The runner starts again as soon as it's done."), vec![]),
323 Code::UpdateFailed => (
324 Tone::Warning,
325 "Update failed".into(),
326 detail,
327 some("The runner keeps running the version it has. Try again, or report it if it keeps happening."),
328 vec![RETRY, REPORT],
329 ),
330 Code::Offline => (
331 Tone::Warning,
332 format!("Offline: {} unreachable", facts.server),
333 some(format!("This computer can't reach {}, so the runner can't take jobs.", facts.server)),
334 some("Check the connection. The runner carries on by itself when it's back."),
335 vec![RETRY],
336 ),
337 Code::Crashed => (
338 Tone::Problem,
339 match facts.exit_code {
340 Some(exit) => format!("Runner stopped (exit code {exit})"),
341 None => "Runner stopped".into(),
342 },
343 detail,
344 Some(match facts.wait_seconds {
345 Some(wait) => format!("It starts again by itself in {wait} seconds. Restart now, or look at its log."),
346 None => "Restart it, or look at its log.".into(),
347 }),
348 vec![RESTART, VIEW_LOG],
349 ),
350 };
351 let capabilities = facts.sandbox.as_ref().map(|status| capabilities(status, facts)).unwrap_or_default();
352 Diagnosis { code, tone, title, cause, fix, actions, capabilities }
353}
354
355/// What the app calls a sandbox's problem with Linux jobs, from the
356/// launcher's sentence about it, which goes with it as its detail.
357pub fn sandbox_code(why: Why) -> Code {
358 match why {
359 Why::NoWsl => Code::NoWsl,
360 Why::WslMirrored => Code::WslMirrored,
361 Why::NoVirtualization => Code::NoVirtualization,
362 Why::NoVfkit => Code::NoVfkit,
363 Why::NoSysboxOrKvm => Code::NoSysbox,
364 Why::Image => Code::ImageFailed,
365 _ => Code::SandboxFailed,
366 }
367}
368
369/// The code for what is wrong with Linux jobs, as a status says it, and
370/// the launcher's own words for it.
371pub fn sandbox_trouble(status: &Status) -> Option<(Code, String, Option<String>)> {
372 let (cause, fix) = status.trouble()?;
373 let code = match launcher::why(&cause) {
374 Why::Other if !status.linux().available => Code::SandboxUnavailable,
375 why => sandbox_code(why),
376 };
377 Some((code, cause, fix))
378}
379
380/// Each kind of job the sandbox runs, on a line of its own.
381fn capabilities(status: &Status, facts: &Facts) -> Vec<CapabilityLine> {
382 let listed = if status.capabilities.is_empty() { vec![status.linux()] } else { status.capabilities.clone() };
383 listed.iter().map(|capability| capability_line(status, capability, facts)).collect()
384}
385
386fn capability_line(status: &Status, capability: &launcher::Capability, facts: &Facts) -> CapabilityLine {
387 let label = match capability.id.as_str() {
388 "linux" => "Linux jobs".to_owned(),
389 "windows" => "Windows jobs".to_owned(),
390 "macos" => "macOS jobs".to_owned(),
391 other => format!("{other} jobs"),
392 };
393 let line = |tone: Tone, title: String, cause: Option<String>, fix: Option<String>, actions: Vec<Action>| CapabilityLine {
394 id: capability.id.clone(),
395 label: label.clone(),
396 tone,
397 title,
398 cause,
399 fix,
400 actions,
401 };
402 let title = if capability.available { "Not running" } else { "Not available" };
403 if capability.id == "linux" {
404 if let Some((code, cause, fix)) = sandbox_trouble(status) {
405 // The same words as the whole runner's when Linux jobs are what is wrong.
406 let said = diagnose(code, &Facts { detail: Some(cause), fix, wait_seconds: None, sandbox: None, ..facts.clone() });
407 return line(Tone::Problem, title.into(), said.cause, said.fix, said.actions);
408 }
409 } else if !capability.available || capability.state == State::Error {
410 let cause = capability.cause.clone();
411 return match cause.as_deref().map(launcher::why) {
412 Some(Why::WindowsSandboxOff) => line(
413 Tone::Off,
414 title.into(),
415 some("Windows Sandbox is off, or this edition of Windows has none: Home doesn't."),
416 some("Linux jobs still run. For Windows jobs, on Windows 11 Pro, Enterprise or Education, turn on Windows Sandbox in Windows features, then restart."),
417 vec![WINDOWS_FEATURES],
418 ),
419 Some(Why::NoWsb) => line(
420 Tone::Off,
421 title.into(),
422 some("This Windows has Windows Sandbox, but not its wsb command, which came with Windows 11 24H2."),
423 some("Linux jobs still run. For Windows jobs, update Windows to 24H2 or later."),
424 vec![],
425 ),
426 Some(Why::MacNotReleased) => line(Tone::Off, title.into(), some("Sandboxed macOS jobs aren't released yet."), some("Linux jobs still run."), vec![]),
427 _ => line(if capability.available { Tone::Problem } else { Tone::Off }, title.into(), cause, capability.fix.clone(), vec![]),
428 };
429 }
430 match capability.state {
431 State::Running => match &capability.job {
432 Some(job) => line(Tone::Working, format!("Running {}", job.said()), None, None, vec![]),
433 None => line(Tone::Ok, "Waiting for jobs".into(), None, None, vec![]),
434 },
435 State::Starting => line(Tone::Working, "Starting".into(), None, None, vec![]),
436 State::Updating => line(Tone::Working, "Updating".into(), None, None, vec![]),
437 State::Stopped => line(Tone::Off, "Stopped".into(), None, None, vec![]),
438 State::Absent => line(Tone::Off, "Not set up yet".into(), None, None, vec![]),
439 State::Error | State::Unknown => line(Tone::Off, "Not running".into(), capability.cause.clone(), capability.fix.clone(), vec![]),
440 }
441}
442
443/// What the site says of this computer while g1t is closed on it.
444pub fn closed(facts: &Facts) -> Diagnosis {
445 Diagnosis {
446 code: Code::Off,
447 tone: Tone::Off,
448 title: format!("g1t is closed on {}", facts.machine),
449 cause: some(format!("This runner runs while g1t is open on {}.", facts.machine)),
450 fix: some(format!("Open g1t on {} to run jobs there again.", facts.machine)),
451 actions: vec![],
452 capabilities: vec![],
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 fn facts() -> Facts {
461 Facts { workspace: Some("acme".into()), backend: "sandboxed", machine: "build-box".into(), server: "g1t.sh".into(), platform: "windows".into(), ..Facts::default() }
462 }
463
464 #[test]
465 fn every_failure_says_why_and_how_to_fix_it() {
466 for code in [Code::SignedOut, Code::NoWorkspace, Code::NotAllowed, Code::RegisterFailed, Code::NoDocker,
467 Code::NoLauncher, Code::NoWsl, Code::WslMirrored, Code::NoVirtualization, Code::NoVfkit, Code::NoSysbox, Code::ImageFailed, Code::SandboxUnavailable, Code::SandboxFailed,
468 Code::SandboxTaken, Code::UpdateFailed, Code::Offline, Code::Crashed,
469 ] {
470 let diagnosis = diagnose(code, &Facts { detail: Some("why".into()), ..facts() });
471 assert!(diagnosis.cause.is_some(), "{code:?} has no cause");
472 assert!(diagnosis.fix.is_some(), "{code:?} has no fix");
473 assert!(!diagnosis.actions.is_empty(), "{code:?} has no action");
474 assert!(matches!(diagnosis.tone, Tone::Warning | Tone::Problem), "{code:?}");
475 }
476 }
477
478 #[test]
479 fn the_words_name_the_workspace_the_machine_and_the_job() {
480 let not_allowed = diagnose(Code::NotAllowed, &facts());
481 assert_eq!(not_allowed.cause.as_deref(), Some("You can't add runners to acme: only its owners can."));
482 assert_eq!(not_allowed.actions[0].id, "choose_workspace");
483 let busy = diagnose(Code::Busy, &Facts { job: Some("Build (acme/web)".into()), ..facts() });
484 assert_eq!(busy.title, "Running Build (acme/web)");
485 assert_eq!(busy.cause.as_deref(), Some("Running sandboxed for acme on build-box."));
486 let crashed = diagnose(Code::Crashed, &Facts { exit_code: Some(101), wait_seconds: Some(30), ..facts() });
487 assert_eq!(crashed.title, "Runner stopped (exit code 101)");
488 assert_eq!(crashed.actions.iter().map(|a| a.id).collect::<Vec<_>>(), ["restart", "view_log"]);
489 assert_eq!(diagnose(Code::NoDocker, &facts()).actions[0].id, "open_docker");
490 assert_eq!(closed(&facts()).title, "g1t is closed on build-box");
491 }
492
493 fn status(json: &str) -> Status {
494 launcher::parse(json).unwrap()
495 }
496
497 const NO_WSL_PRO: &str = r#"{"schema":1,"state":"error","backend":"wsl2","launcher_version":"0.2.1",
498 "error":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.",
499 "capabilities":[
500 {"id":"linux","available":false,"state":"error","isolation":"nested-container",
501 "cause":"WSL is not installed, and the sandbox for Linux jobs is a WSL distribution of its own.",
502 "fix":"Open a terminal as administrator and run `wsl --install --no-distribution`, then restart Windows. It works on every edition of Windows 11, Home included."},
503 {"id":"windows","available":true,"state":"running","isolation":"vm","runner_version":"0.2.1","job":null}]}"#;
504
505 #[test]
506 fn without_wsl_the_fix_is_the_command_to_copy_and_windows_jobs_still_show() {
507 let status = status(NO_WSL_PRO);
508 let (code, cause, _) = sandbox_trouble(&status).unwrap();
509 assert_eq!(code, Code::NoWsl);
510 let diagnosis = diagnose(code, &Facts { detail: Some(cause), sandbox: Some(status), ..facts() });
511 assert_eq!(diagnosis.title, "Not running: WSL isn't installed");
512 assert_eq!(diagnosis.fix.as_deref(), Some("Run `wsl --install --no-distribution` in an admin terminal, then restart Windows. It works on every edition of Windows 11, Home included."));
513 assert_eq!(diagnosis.actions[0], COPY_WSL_INSTALL);
514 let [linux, windows] = &diagnosis.capabilities[..] else { panic!("{:?}", diagnosis.capabilities) };
515 assert_eq!((linux.label.as_str(), linux.title.as_str(), linux.tone), ("Linux jobs", "Not available", Tone::Problem));
516 assert_eq!(linux.fix, diagnosis.fix);
517 assert_eq!(linux.actions[0].id, "copy_wsl_install");
518 assert_eq!((windows.label.as_str(), windows.title.as_str(), windows.tone), ("Windows jobs", "Waiting for jobs", Tone::Ok));
519 }
520
521 #[test]
522 fn on_home_windows_jobs_are_unavailable_and_linux_jobs_still_run() {
523 let status = status(
524 r#"{"schema":1,"state":"running","backend":"wsl2","runner_version":"0.3.0","launcher_version":"0.3.0",
525 "job":{"kind":"workflow","id":"job_1","name":"build","repo":"acme/web"},
526 "capabilities":[
527 {"id":"linux","available":true,"state":"running","runner_version":"0.3.0","job":{"kind":"workflow","id":"job_1","name":"build","repo":"acme/web"}},
528 {"id":"windows","available":false,"state":"absent","cause":"Windows Sandbox is not turned on, or this edition of Windows has none (Home does not).","fix":"On Windows 11 Pro…"}]}"#,
529 );
530 assert_eq!(sandbox_trouble(&status), None);
531 let busy = diagnose(Code::Busy, &Facts { job: status.job().map(launcher::Job::said), sandbox: Some(status), ..facts() });
532 assert_eq!(busy.title, "Running build (acme/web)");
533 let [linux, windows] = &busy.capabilities[..] else { panic!() };
534 assert_eq!((linux.title.as_str(), linux.tone), ("Running build (acme/web)", Tone::Working));
535 assert_eq!((windows.title.as_str(), windows.tone), ("Not available", Tone::Off));
536 assert_eq!(windows.cause.as_deref(), Some("Windows Sandbox is off, or this edition of Windows has none: Home doesn't."));
537 assert!(windows.fix.as_deref().unwrap().starts_with("Linux jobs still run."));
538 assert_eq!(windows.actions[0].id, "windows_features");
539 }
540
541 #[test]
542 fn each_launcher_trouble_has_its_words() {
543 let trouble = |linux_available: bool, text: &str| {
544 let json = serde_json::json!({
545 "schema": 1, "state": "error", "error": text,
546 "capabilities": [{"id": "linux", "available": linux_available, "state": "error", "cause": if linux_available { None } else { Some(text) }, "fix": "the launcher's fix"}]
547 });
548 sandbox_trouble(&status(&json.to_string())).unwrap()
549 };
550 assert_eq!(trouble(true, "WSL could not import the sandbox: Please enable the Virtual Machine Platform Windows feature and ensure virtualization is enabled in the BIOS.").0, Code::NoVirtualization);
551 assert_eq!(trouble(false, "vfkit, which runs the sandbox's VM with Apple's Virtualization framework, is not installed.").0, Code::NoVfkit);
552 assert_eq!(trouble(false, "This machine has neither Sysbox nor KVM, so there is nothing to sandbox the runner with.").0, Code::NoSysbox);
553 assert_eq!(trouble(false, "WSL is set to mirrored networking, in which Linux reaches this computer's own localhost.").0, Code::WslMirrored);
554 assert_eq!(trouble(true, "the sandbox image wsl-x64.tar.gz's SHA-256 is not the release's; not using it").0, Code::ImageFailed);
555 // Unknown words: shown as the launcher wrote them, its fix with them.
556 let (code, cause, fix) = trouble(false, "This user may not use /dev/kvm.");
557 assert_eq!(code, Code::SandboxUnavailable);
558 let unavailable = diagnose(code, &Facts { detail: Some(cause), fix, ..facts() });
559 assert_eq!(unavailable.cause.as_deref(), Some("This user may not use /dev/kvm."));
560 assert_eq!(unavailable.fix.as_deref(), Some("the launcher's fix"));
561 let (code, cause, _) = trouble(true, "the sandbox did not start within 600 seconds");
562 assert_eq!(code, Code::SandboxFailed);
563 let failed = diagnose(code, &Facts { detail: Some(cause), wait_seconds: Some(60), ..facts() });
564 assert_eq!(failed.cause.as_deref(), Some("the sandbox did not start within 600 seconds"));
565 assert_eq!(failed.fix.as_deref(), Some("It starts again by itself in 60 seconds. Retry now, or look at its log."));
566 let image = diagnose(Code::ImageFailed, &Facts { detail: Some("could not fetch https://g1t.sh/x: dns error".into()), ..facts() });
567 assert_eq!(image.cause.as_deref(), Some("could not fetch https://g1t.sh/x: dns error"));
568 assert_eq!(diagnose(Code::NoVirtualization, &Facts { platform: "macos".into(), ..facts() }).actions[0].id, "retry");
569 assert_eq!(diagnose(Code::NoVfkit, &facts()).actions[0].id, "copy_vfkit_install");
570 }
571
572 #[test]
573 fn a_diagnosis_without_the_sandbox_lists_no_capabilities() {
574 let json = serde_json::to_value(diagnose(Code::Idle, &Facts { backend: "in Docker", ..facts() })).unwrap();
575 assert!(json.get("capabilities").is_none());
576 let sandboxed = serde_json::to_value(diagnose(Code::Idle, &Facts { sandbox: Some(status(NO_WSL_PRO)), ..facts() })).unwrap();
577 assert_eq!(sandboxed["capabilities"][1]["id"], "windows");
578 }
579}