1use serde::{Deserialize, Serialize};
40use std::time::Duration;
41
42use crate::errors::catalog::ErrorCode;
43#[cfg(all(feature = "rich-ui", unix))]
44use crate::ui::RchTheme;
45use crate::ui::{ErrorPanel, Icons, OutputContext};
46
47#[cfg(all(feature = "rich-ui", unix))]
48use rich_rust::r#box::HEAVY;
49#[cfg(all(feature = "rich-ui", unix))]
50use rich_rust::prelude::*;
51
52#[derive(Debug, Clone, Serialize, Deserialize, Default)]
54pub struct WorkerResourceState {
55 #[serde(skip_serializing_if = "Option::is_none")]
57 pub cpu_percent: Option<f64>,
58 #[serde(skip_serializing_if = "Option::is_none")]
60 pub memory_used_gb: Option<f64>,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub memory_total_gb: Option<f64>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub load_average: Option<f64>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub disk_percent: Option<f64>,
70}
71
72impl WorkerResourceState {
73 #[must_use]
75 pub fn has_data(&self) -> bool {
76 self.cpu_percent.is_some()
77 || self.memory_used_gb.is_some()
78 || self.load_average.is_some()
79 || self.disk_percent.is_some()
80 }
81
82 #[must_use]
84 pub fn format_line(&self) -> String {
85 let mut parts = Vec::new();
86
87 if let Some(cpu) = self.cpu_percent {
88 parts.push(format!("CPU: {cpu:.0}%"));
89 }
90
91 if let (Some(used), Some(total)) = (self.memory_used_gb, self.memory_total_gb) {
92 parts.push(format!("Memory: {used:.1}/{total:.1} GB"));
93 } else if let Some(used) = self.memory_used_gb {
94 parts.push(format!("Memory: {used:.1} GB"));
95 }
96
97 if let Some(load) = self.load_average {
98 parts.push(format!("Load: {load:.1}"));
99 }
100
101 if let Some(disk) = self.disk_percent {
102 parts.push(format!("Disk: {disk:.0}%"));
103 }
104
105 parts.join(" │ ")
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct SignalInfo {
112 pub signal_number: i32,
114 pub signal_name: String,
116 pub likely_oom: bool,
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub details: Option<String>,
121}
122
123impl SignalInfo {
124 #[must_use]
126 pub fn from_signal(signal: i32) -> Self {
127 let signal_name = match signal {
128 1 => "SIGHUP",
129 2 => "SIGINT",
130 3 => "SIGQUIT",
131 6 => "SIGABRT",
132 9 => "SIGKILL",
133 11 => "SIGSEGV",
134 13 => "SIGPIPE",
135 14 => "SIGALRM",
136 15 => "SIGTERM",
137 _ => "UNKNOWN",
138 };
139
140 let likely_oom = signal == 9;
142
143 Self {
144 signal_number: signal,
145 signal_name: signal_name.to_string(),
146 likely_oom,
147 details: None,
148 }
149 }
150
151 #[must_use]
153 pub fn from_exit_code(exit_code: i32) -> Option<Self> {
154 if exit_code > 128 && exit_code <= 128 + 64 {
155 Some(Self::from_signal(exit_code - 128))
156 } else {
157 None
158 }
159 }
160
161 #[must_use]
163 pub fn with_oom_details(mut self, details: impl Into<String>) -> Self {
164 self.likely_oom = true;
165 self.details = Some(details.into());
166 self
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct BuildErrorDisplay {
180 pub error_code: ErrorCode,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
185 pub command: Option<String>,
186
187 #[serde(skip_serializing_if = "Option::is_none")]
189 pub worker_name: Option<String>,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub duration: Option<Duration>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub timeout: Option<Duration>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub last_output: Option<String>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub compiler_output: Option<String>,
206
207 #[serde(default, skip_serializing_if = "is_default_resources")]
209 pub resources: WorkerResourceState,
210
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub signal_info: Option<SignalInfo>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub exit_code: Option<i32>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub artifact_path: Option<String>,
222
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub workdir: Option<String>,
226
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub toolchain: Option<String>,
230
231 #[serde(default)]
233 pub is_remote: bool,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub custom_message: Option<String>,
238
239 #[serde(default, skip_serializing_if = "Vec::is_empty")]
241 pub caused_by: Vec<String>,
242}
243
244fn is_default_resources(r: &WorkerResourceState) -> bool {
245 !r.has_data()
246}
247
248impl BuildErrorDisplay {
249 #[must_use]
255 pub fn compilation_failed(command: impl Into<String>) -> Self {
256 Self::new(ErrorCode::BuildCompilationFailed).command(command)
257 }
258
259 #[must_use]
261 pub fn unknown_command(command: impl Into<String>) -> Self {
262 Self::new(ErrorCode::BuildUnknownCommand).command(command)
263 }
264
265 #[must_use]
267 pub fn killed_by_signal(signal: i32) -> Self {
268 let signal_info = SignalInfo::from_signal(signal);
269 let mut display = Self::new(ErrorCode::BuildKilledBySignal);
270 display.signal_info = Some(signal_info);
271 display
272 }
273
274 #[must_use]
276 pub fn killed_from_exit_code(exit_code: i32) -> Self {
277 if let Some(signal_info) = SignalInfo::from_exit_code(exit_code) {
278 let mut display = Self::new(ErrorCode::BuildKilledBySignal);
279 display.signal_info = Some(signal_info);
280 display.exit_code = Some(exit_code);
281 display
282 } else {
283 let mut display = Self::new(ErrorCode::BuildCompilationFailed);
284 display.exit_code = Some(exit_code);
285 display
286 }
287 }
288
289 #[must_use]
291 pub fn build_timeout(command: impl Into<String>) -> Self {
292 Self::new(ErrorCode::BuildTimeout).command(command)
293 }
294
295 #[must_use]
297 pub fn output_error() -> Self {
298 Self::new(ErrorCode::BuildOutputError)
299 }
300
301 #[must_use]
303 pub fn workdir_error(workdir: impl Into<String>) -> Self {
304 let mut display = Self::new(ErrorCode::BuildWorkdirError);
305 display.workdir = Some(workdir.into());
306 display
307 }
308
309 #[must_use]
311 pub fn toolchain_error(toolchain: impl Into<String>) -> Self {
312 let mut display = Self::new(ErrorCode::BuildToolchainError);
313 display.toolchain = Some(toolchain.into());
314 display
315 }
316
317 #[must_use]
319 pub fn env_error() -> Self {
320 Self::new(ErrorCode::BuildEnvError)
321 }
322
323 #[must_use]
325 pub fn incremental_error() -> Self {
326 Self::new(ErrorCode::BuildIncrementalError)
327 }
328
329 #[must_use]
331 pub fn artifact_missing(artifact_path: impl Into<String>) -> Self {
332 let mut display = Self::new(ErrorCode::BuildArtifactMissing);
333 display.artifact_path = Some(artifact_path.into());
334 display
335 }
336
337 #[must_use]
343 fn new(error_code: ErrorCode) -> Self {
344 Self {
345 error_code,
346 command: None,
347 worker_name: None,
348 duration: None,
349 timeout: None,
350 last_output: None,
351 compiler_output: None,
352 resources: WorkerResourceState::default(),
353 signal_info: None,
354 exit_code: None,
355 artifact_path: None,
356 workdir: None,
357 toolchain: None,
358 is_remote: true,
359 custom_message: None,
360 caused_by: Vec::new(),
361 }
362 }
363
364 #[must_use]
370 pub fn command(mut self, command: impl Into<String>) -> Self {
371 self.command = Some(command.into());
372 self
373 }
374
375 #[must_use]
377 pub fn worker(mut self, worker: impl Into<String>) -> Self {
378 self.worker_name = Some(worker.into());
379 self
380 }
381
382 #[must_use]
384 pub fn duration(mut self, duration: Duration) -> Self {
385 self.duration = Some(duration);
386 self
387 }
388
389 #[must_use]
391 pub fn duration_secs(self, secs: u64) -> Self {
392 self.duration(Duration::from_secs(secs))
393 }
394
395 #[must_use]
397 pub fn timeout(mut self, timeout: Duration) -> Self {
398 self.timeout = Some(timeout);
399 self
400 }
401
402 #[must_use]
404 pub fn timeout_secs(self, secs: u64) -> Self {
405 self.timeout(Duration::from_secs(secs))
406 }
407
408 #[must_use]
410 pub fn last_output(mut self, output: impl Into<String>) -> Self {
411 self.last_output = Some(output.into());
412 self
413 }
414
415 #[must_use]
417 pub fn compiler_output(mut self, output: impl Into<String>) -> Self {
418 self.compiler_output = Some(output.into());
419 self
420 }
421
422 #[must_use]
424 pub fn cpu_usage(mut self, percent: f64) -> Self {
425 self.resources.cpu_percent = Some(percent);
426 self
427 }
428
429 #[must_use]
431 pub fn memory_usage(mut self, used_gb: f64, total_gb: f64) -> Self {
432 self.resources.memory_used_gb = Some(used_gb);
433 self.resources.memory_total_gb = Some(total_gb);
434 self
435 }
436
437 #[must_use]
439 pub fn load_average(mut self, load: f64) -> Self {
440 self.resources.load_average = Some(load);
441 self
442 }
443
444 #[must_use]
446 pub fn disk_usage(mut self, percent: f64) -> Self {
447 self.resources.disk_percent = Some(percent);
448 self
449 }
450
451 #[must_use]
453 pub fn exit_code(mut self, code: i32) -> Self {
454 self.exit_code = Some(code);
455 self
456 }
457
458 #[must_use]
460 pub fn local(mut self) -> Self {
461 self.is_remote = false;
462 self
463 }
464
465 #[must_use]
467 pub fn workdir(mut self, path: impl Into<String>) -> Self {
468 self.workdir = Some(path.into());
469 self
470 }
471
472 #[must_use]
474 pub fn toolchain(mut self, tc: impl Into<String>) -> Self {
475 self.toolchain = Some(tc.into());
476 self
477 }
478
479 #[must_use]
481 pub fn message(mut self, message: impl Into<String>) -> Self {
482 self.custom_message = Some(message.into());
483 self
484 }
485
486 #[must_use]
488 pub fn caused_by(mut self, cause: impl Into<String>) -> Self {
489 self.caused_by.push(cause.into());
490 self
491 }
492
493 #[must_use]
495 pub fn mark_oom(mut self, details: impl Into<String>) -> Self {
496 if let Some(ref mut info) = self.signal_info {
497 info.likely_oom = true;
498 info.details = Some(details.into());
499 } else {
500 self.signal_info = Some(SignalInfo::from_signal(9).with_oom_details(details));
501 }
502 self
503 }
504
505 #[must_use]
511 pub fn to_error_panel(&self) -> ErrorPanel {
512 let entry = self.error_code.entry();
513
514 let mut panel = ErrorPanel::error(&entry.code, &entry.message);
515
516 if let Some(ref msg) = self.custom_message {
518 panel = panel.message(msg.clone());
519 }
520
521 if let Some(ref cmd) = self.command {
523 panel = panel.context("Command", cmd.clone());
524 }
525
526 if let Some(ref worker) = self.worker_name {
528 let location = if self.is_remote { "remote" } else { "local" };
529 panel = panel.context("Worker", format!("{worker} ({location})"));
530 }
531
532 if let (Some(dur), Some(timeout)) = (self.duration, self.timeout) {
534 panel = panel.context(
535 "Duration",
536 format!("{}s (timeout: {}s)", dur.as_secs(), timeout.as_secs()),
537 );
538 } else if let Some(dur) = self.duration {
539 panel = panel.context("Duration", format!("{}s", dur.as_secs()));
540 }
541
542 if let Some(code) = self.exit_code {
544 panel = panel.context("Exit code", code.to_string());
545 }
546
547 if let Some(ref sig) = self.signal_info {
549 let sig_text = if sig.likely_oom {
550 format!(
551 "{} (signal {}) - likely OOM",
552 sig.signal_name, sig.signal_number
553 )
554 } else {
555 format!("{} (signal {})", sig.signal_name, sig.signal_number)
556 };
557 panel = panel.context("Signal", sig_text);
558 }
559
560 if let Some(ref tc) = self.toolchain {
562 panel = panel.context("Toolchain", tc.clone());
563 }
564
565 if let Some(ref wd) = self.workdir {
567 panel = panel.context("Working directory", wd.clone());
568 }
569
570 if let Some(ref path) = self.artifact_path {
572 panel = panel.context("Artifact path", path.clone());
573 }
574
575 for cause in &self.caused_by {
577 panel = panel.caused_by(cause.clone(), None);
578 }
579
580 for step in entry.remediation {
582 panel = panel.suggestion(step);
583 }
584
585 panel
586 }
587
588 pub fn render(&self, ctx: OutputContext) {
597 if ctx.is_machine() {
598 return;
600 }
601
602 if let Some(ref compiler_output) = self.compiler_output {
604 self.render_with_compiler_output(ctx, compiler_output);
605 return;
606 }
607
608 #[cfg(all(feature = "rich-ui", unix))]
609 if ctx.supports_rich() {
610 self.render_rich(ctx);
611 return;
612 }
613
614 self.render_plain(ctx);
615 }
616
617 fn render_with_compiler_output(&self, ctx: OutputContext, compiler_output: &str) {
619 let entry = self.error_code.entry();
620 let icon = Icons::cross(ctx);
621
622 eprintln!();
624 eprintln!(
625 "{icon} [RCH] Build failed on {}",
626 self.worker_name.as_deref().unwrap_or("remote")
627 );
628
629 if let Some(ref cmd) = self.command {
630 eprintln!(" Command: {cmd}");
631 }
632 if let Some(dur) = self.duration {
633 eprintln!(" Duration: {}s", dur.as_secs());
634 }
635 eprintln!();
636
637 eprint!("{compiler_output}");
639
640 eprintln!();
642 eprintln!("{icon} {} - {}", entry.code, entry.message);
643
644 if self.resources.has_data() {
645 eprintln!(" Worker state: {}", self.resources.format_line());
646 }
647
648 if !entry.remediation.is_empty() {
649 eprintln!();
650 eprintln!("Suggestions:");
651 for (i, step) in entry.remediation.iter().enumerate() {
652 eprintln!(" {}. {step}", i + 1);
653 }
654 }
655 }
656
657 #[cfg(all(feature = "rich-ui", unix))]
659 fn render_rich(&self, ctx: OutputContext) {
660 let content = self.build_rich_content(ctx);
661 let entry = self.error_code.entry();
662 let icon = Icons::cross(ctx);
663 let title_text = format!("{icon} {}: {}", entry.code, entry.message);
664
665 let border_color = Color::parse(RchTheme::ERROR).unwrap_or_else(|_| Color::default());
666 let border_style = Style::new().bold().color(border_color);
667
668 let panel = Panel::from_text(&content)
669 .title(title_text.as_str())
670 .border_style(border_style)
671 .box_style(&HEAVY);
672
673 let console = Console::builder().force_terminal(true).build();
674 console.print_renderable(&panel);
675 }
676
677 #[cfg(all(feature = "rich-ui", unix))]
679 fn build_rich_content(&self, _ctx: OutputContext) -> String {
680 let mut lines = Vec::new();
681
682 if let Some(ref msg) = self.custom_message {
684 lines.push(msg.clone());
685 } else if let Some(ref sig) = self.signal_info {
686 if sig.likely_oom {
687 lines.push(format!(
688 "Build process was killed by {} - likely out of memory",
689 sig.signal_name
690 ));
691 } else {
692 lines.push(format!("Build process was killed by {}", sig.signal_name));
693 }
694 }
695
696 if let Some(ref cmd) = self.command {
698 lines.push(String::new());
699 lines.push(format!("[{}]Command:[/] {cmd}", RchTheme::DIM));
700 }
701
702 if let Some(ref worker) = self.worker_name {
704 let location = if self.is_remote { "remote" } else { "local" };
705 lines.push(format!(
706 "[{}]Worker:[/] {worker} ({location})",
707 RchTheme::DIM
708 ));
709 }
710
711 if let (Some(dur), Some(timeout)) = (self.duration, self.timeout) {
712 lines.push(format!(
713 "[{}]Duration:[/] {}s (timeout: {}s)",
714 RchTheme::DIM,
715 dur.as_secs(),
716 timeout.as_secs()
717 ));
718 } else if let Some(dur) = self.duration {
719 lines.push(format!(
720 "[{}]Duration:[/] {}s",
721 RchTheme::DIM,
722 dur.as_secs()
723 ));
724 }
725
726 if let Some(ref output) = self.last_output {
728 lines.push(String::new());
729 lines.push(format!("[{}]Last output:[/]", RchTheme::DIM));
730 lines.push(format!(" {output}"));
731 }
732
733 if self.resources.has_data() {
735 lines.push(String::new());
736 lines.push(format!("[{}]Worker state at error:[/]", RchTheme::DIM));
737 lines.push(format!(" {}", self.resources.format_line()));
738 }
739
740 if let Some(ref sig) = self.signal_info
742 && let Some(ref details) = sig.details
743 {
744 lines.push(String::new());
745 lines.push(format!("[{}]Signal details:[/]", RchTheme::DIM));
746 lines.push(format!(" {details}"));
747 }
748
749 if !self.caused_by.is_empty() {
751 lines.push(String::new());
752 lines.push(format!("[{}]Caused by:[/]", RchTheme::DIM));
753 for cause in &self.caused_by {
754 lines.push(format!(" {cause}"));
755 }
756 }
757
758 let entry = self.error_code.entry();
760 if !entry.remediation.is_empty() {
761 lines.push(String::new());
762 lines.push(format!("[{}]Suggestions:[/]", RchTheme::SECONDARY));
763 for (i, step) in entry.remediation.iter().enumerate() {
764 lines.push(format!(" [{}]{}.[/] {step}", RchTheme::SECONDARY, i + 1));
765 }
766 }
767
768 lines.join("\n")
769 }
770
771 fn render_plain(&self, ctx: OutputContext) {
773 let entry = self.error_code.entry();
774 let icon = Icons::cross(ctx);
775
776 eprintln!("{icon} [ERROR] {}: {}", entry.code, entry.message);
778
779 if let Some(ref msg) = self.custom_message {
781 eprintln!();
782 eprintln!("{msg}");
783 } else if let Some(ref sig) = self.signal_info {
784 eprintln!();
785 if sig.likely_oom {
786 eprintln!(
787 "Build process was killed by {} (signal {}) - likely out of memory",
788 sig.signal_name, sig.signal_number
789 );
790 } else {
791 eprintln!(
792 "Build process was killed by {} (signal {})",
793 sig.signal_name, sig.signal_number
794 );
795 }
796 }
797
798 if let Some(ref cmd) = self.command {
800 eprintln!();
801 eprintln!("Command: {cmd}");
802 }
803
804 if let Some(ref worker) = self.worker_name {
806 let location = if self.is_remote { "remote" } else { "local" };
807 eprintln!("Worker: {worker} ({location})");
808 }
809
810 if let (Some(dur), Some(timeout)) = (self.duration, self.timeout) {
811 eprintln!(
812 "Duration: {}s (timeout: {}s)",
813 dur.as_secs(),
814 timeout.as_secs()
815 );
816 } else if let Some(dur) = self.duration {
817 eprintln!("Duration: {}s", dur.as_secs());
818 }
819
820 if let Some(code) = self.exit_code {
822 eprintln!("Exit code: {code}");
823 }
824
825 if let Some(ref tc) = self.toolchain {
827 eprintln!("Toolchain: {tc}");
828 }
829 if let Some(ref wd) = self.workdir {
830 eprintln!("Working directory: {wd}");
831 }
832 if let Some(ref path) = self.artifact_path {
833 eprintln!("Artifact path: {path}");
834 }
835
836 if let Some(ref output) = self.last_output {
838 eprintln!();
839 eprintln!("Last output:");
840 eprintln!(" {output}");
841 }
842
843 if self.resources.has_data() {
845 eprintln!();
846 eprintln!("Worker state at error:");
847 eprintln!(" {}", self.resources.format_line());
848 }
849
850 if let Some(ref sig) = self.signal_info
852 && let Some(ref details) = sig.details
853 {
854 eprintln!();
855 eprintln!("Signal details: {details}");
856 }
857
858 if !self.caused_by.is_empty() {
860 eprintln!();
861 eprintln!("Caused by:");
862 for cause in &self.caused_by {
863 eprintln!(" {cause}");
864 }
865 }
866
867 if !entry.remediation.is_empty() {
869 eprintln!();
870 eprintln!("Suggestions:");
871 for (i, step) in entry.remediation.iter().enumerate() {
872 eprintln!(" {}. {step}", i + 1);
873 }
874 }
875 }
876
877 pub fn to_json(&self) -> serde_json::Result<String> {
883 serde_json::to_string_pretty(self)
884 }
885
886 pub fn to_json_compact(&self) -> serde_json::Result<String> {
888 serde_json::to_string(self)
889 }
890}
891
892impl std::fmt::Display for BuildErrorDisplay {
893 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
894 let entry = self.error_code.entry();
895 write!(f, "[ERROR] {}: {}", entry.code, entry.message)?;
896 if let Some(ref msg) = self.custom_message {
897 write!(f, " - {msg}")?;
898 }
899 Ok(())
900 }
901}
902
903impl std::error::Error for BuildErrorDisplay {}
904
905#[cfg(test)]
906mod tests {
907 use super::*;
908
909 #[test]
910 fn test_compilation_failed_creation() {
911 let display = BuildErrorDisplay::compilation_failed("cargo build");
912 assert_eq!(display.error_code, ErrorCode::BuildCompilationFailed);
913 assert_eq!(display.command, Some("cargo build".to_string()));
914 }
915
916 #[test]
917 fn test_build_timeout_creation() {
918 let display = BuildErrorDisplay::build_timeout("cargo build --release")
919 .worker("build1")
920 .duration_secs(300)
921 .timeout_secs(300);
922
923 assert_eq!(display.error_code, ErrorCode::BuildTimeout);
924 assert_eq!(display.command, Some("cargo build --release".to_string()));
925 assert_eq!(display.worker_name, Some("build1".to_string()));
926 assert_eq!(display.duration, Some(Duration::from_secs(300)));
927 assert_eq!(display.timeout, Some(Duration::from_secs(300)));
928 }
929
930 #[test]
931 fn test_killed_by_signal() {
932 let display = BuildErrorDisplay::killed_by_signal(9);
933 assert_eq!(display.error_code, ErrorCode::BuildKilledBySignal);
934 assert!(display.signal_info.is_some());
935
936 let sig = display.signal_info.unwrap();
937 assert_eq!(sig.signal_number, 9);
938 assert_eq!(sig.signal_name, "SIGKILL");
939 assert!(sig.likely_oom);
940 }
941
942 #[test]
943 fn test_killed_from_exit_code() {
944 let display = BuildErrorDisplay::killed_from_exit_code(137);
946 assert_eq!(display.error_code, ErrorCode::BuildKilledBySignal);
947 assert_eq!(display.exit_code, Some(137));
948
949 let sig = display.signal_info.unwrap();
950 assert_eq!(sig.signal_number, 9);
951 }
952
953 #[test]
954 fn test_killed_from_regular_exit_code() {
955 let display = BuildErrorDisplay::killed_from_exit_code(1);
957 assert_eq!(display.error_code, ErrorCode::BuildCompilationFailed);
958 assert!(display.signal_info.is_none());
959 }
960
961 #[test]
962 fn test_worker_resources() {
963 let display = BuildErrorDisplay::build_timeout("cargo build")
964 .cpu_usage(98.0)
965 .memory_usage(14.2, 16.0)
966 .load_average(8.5)
967 .disk_usage(75.0);
968
969 assert!(display.resources.has_data());
970 assert_eq!(display.resources.cpu_percent, Some(98.0));
971 assert_eq!(display.resources.memory_used_gb, Some(14.2));
972 assert_eq!(display.resources.memory_total_gb, Some(16.0));
973 assert_eq!(display.resources.load_average, Some(8.5));
974 assert_eq!(display.resources.disk_percent, Some(75.0));
975 }
976
977 #[test]
978 fn test_resource_format_line() {
979 let resources = WorkerResourceState {
980 cpu_percent: Some(98.0),
981 memory_used_gb: Some(14.2),
982 memory_total_gb: Some(16.0),
983 load_average: Some(8.5),
984 ..Default::default()
985 };
986
987 let line = resources.format_line();
988 assert!(line.contains("CPU: 98%"));
989 assert!(line.contains("Memory: 14.2/16.0 GB"));
990 assert!(line.contains("Load: 8.5"));
991 }
992
993 #[test]
994 fn test_artifact_missing() {
995 let display = BuildErrorDisplay::artifact_missing("target/release/myapp");
996 assert_eq!(display.error_code, ErrorCode::BuildArtifactMissing);
997 assert_eq!(
998 display.artifact_path,
999 Some("target/release/myapp".to_string())
1000 );
1001 }
1002
1003 #[test]
1004 fn test_toolchain_error() {
1005 let display = BuildErrorDisplay::toolchain_error("nightly-2024-01-15");
1006 assert_eq!(display.error_code, ErrorCode::BuildToolchainError);
1007 assert_eq!(display.toolchain, Some("nightly-2024-01-15".to_string()));
1008 }
1009
1010 #[test]
1011 fn test_workdir_error() {
1012 let display = BuildErrorDisplay::workdir_error("/tmp/rch/project");
1013 assert_eq!(display.error_code, ErrorCode::BuildWorkdirError);
1014 assert_eq!(display.workdir, Some("/tmp/rch/project".to_string()));
1015 }
1016
1017 #[test]
1018 fn test_builder_chain() {
1019 let display = BuildErrorDisplay::compilation_failed("cargo test")
1020 .worker("build2")
1021 .duration_secs(45)
1022 .exit_code(101)
1023 .last_output("test result: FAILED. 3 passed; 2 failed;")
1024 .caused_by("Test assertion failed")
1025 .message("Tests failed on remote worker");
1026
1027 assert_eq!(display.worker_name, Some("build2".to_string()));
1028 assert_eq!(display.duration, Some(Duration::from_secs(45)));
1029 assert_eq!(display.exit_code, Some(101));
1030 assert!(display.last_output.is_some());
1031 assert_eq!(display.caused_by.len(), 1);
1032 assert!(display.custom_message.is_some());
1033 }
1034
1035 #[test]
1036 fn test_local_build() {
1037 let display = BuildErrorDisplay::compilation_failed("cargo build").local();
1038 assert!(!display.is_remote);
1039 }
1040
1041 #[test]
1042 fn test_mark_oom() {
1043 let display = BuildErrorDisplay::killed_by_signal(9)
1044 .mark_oom("OOM killer triggered at 15.9GB memory usage");
1045
1046 let sig = display.signal_info.unwrap();
1047 assert!(sig.likely_oom);
1048 assert!(sig.details.is_some());
1049 assert!(sig.details.unwrap().contains("OOM killer"));
1050 }
1051
1052 #[test]
1053 fn test_to_error_panel() {
1054 let display = BuildErrorDisplay::build_timeout("cargo build --release")
1055 .worker("build1")
1056 .duration_secs(300)
1057 .timeout_secs(300);
1058
1059 let panel = display.to_error_panel();
1060 assert_eq!(panel.code, "RCH-E303");
1061 }
1062
1063 #[test]
1064 fn test_json_serialization() {
1065 let display = BuildErrorDisplay::build_timeout("cargo build")
1066 .worker("build1")
1067 .cpu_usage(95.0);
1068
1069 let json = display.to_json().expect("JSON serialization failed");
1070 assert!(json.contains("cargo build"));
1071 assert!(json.contains("build1"));
1072 assert!(json.contains("95"));
1073 }
1074
1075 #[test]
1076 fn test_json_compact() {
1077 let display = BuildErrorDisplay::compilation_failed("cargo test");
1078 let json = display
1079 .to_json_compact()
1080 .expect("JSON serialization failed");
1081 assert!(!json.contains('\n'));
1082 }
1083
1084 #[test]
1085 fn test_display_implementation() {
1086 let display =
1087 BuildErrorDisplay::build_timeout("cargo build").message("Custom timeout message");
1088
1089 let output = format!("{display}");
1090 assert!(output.contains("RCH-E303"));
1091 assert!(output.contains("Custom timeout message"));
1092 }
1093
1094 #[test]
1095 fn test_render_plain_no_panic() {
1096 let display = BuildErrorDisplay::build_timeout("cargo build --release")
1097 .worker("build1")
1098 .duration_secs(300)
1099 .timeout_secs(300)
1100 .last_output("Compiling serde_derive v1.0.152")
1101 .cpu_usage(98.0)
1102 .memory_usage(14.2, 16.0);
1103
1104 display.render(OutputContext::Plain);
1106 }
1107
1108 #[test]
1109 fn test_render_machine_silent() {
1110 let display = BuildErrorDisplay::compilation_failed("cargo build");
1111 display.render(OutputContext::Machine);
1113 }
1114
1115 #[test]
1116 fn test_signal_info_from_exit_code_invalid() {
1117 assert!(SignalInfo::from_exit_code(128).is_none());
1119 assert!(SignalInfo::from_exit_code(200).is_none());
1121 assert!(SignalInfo::from_exit_code(137).is_some()); assert!(SignalInfo::from_exit_code(143).is_some()); }
1125
1126 #[test]
1127 fn test_all_error_constructors() {
1128 assert_eq!(
1129 BuildErrorDisplay::compilation_failed("cmd").error_code,
1130 ErrorCode::BuildCompilationFailed
1131 );
1132 assert_eq!(
1133 BuildErrorDisplay::unknown_command("cmd").error_code,
1134 ErrorCode::BuildUnknownCommand
1135 );
1136 assert_eq!(
1137 BuildErrorDisplay::killed_by_signal(9).error_code,
1138 ErrorCode::BuildKilledBySignal
1139 );
1140 assert_eq!(
1141 BuildErrorDisplay::build_timeout("cmd").error_code,
1142 ErrorCode::BuildTimeout
1143 );
1144 assert_eq!(
1145 BuildErrorDisplay::output_error().error_code,
1146 ErrorCode::BuildOutputError
1147 );
1148 assert_eq!(
1149 BuildErrorDisplay::workdir_error("path").error_code,
1150 ErrorCode::BuildWorkdirError
1151 );
1152 assert_eq!(
1153 BuildErrorDisplay::toolchain_error("nightly").error_code,
1154 ErrorCode::BuildToolchainError
1155 );
1156 assert_eq!(
1157 BuildErrorDisplay::env_error().error_code,
1158 ErrorCode::BuildEnvError
1159 );
1160 assert_eq!(
1161 BuildErrorDisplay::incremental_error().error_code,
1162 ErrorCode::BuildIncrementalError
1163 );
1164 assert_eq!(
1165 BuildErrorDisplay::artifact_missing("path").error_code,
1166 ErrorCode::BuildArtifactMissing
1167 );
1168 }
1169
1170 #[test]
1171 fn test_error_trait() {
1172 let display: Box<dyn std::error::Error> =
1173 Box::new(BuildErrorDisplay::compilation_failed("cargo build"));
1174 let _ = format!("{display}");
1175 }
1176
1177 #[test]
1178 fn test_empty_resources() {
1179 let resources = WorkerResourceState::default();
1180 assert!(!resources.has_data());
1181 assert!(resources.format_line().is_empty());
1182 }
1183
1184 #[test]
1185 fn test_partial_resources() {
1186 let resources = WorkerResourceState {
1187 cpu_percent: Some(50.0),
1188 ..Default::default()
1189 };
1190 assert!(resources.has_data());
1191
1192 let line = resources.format_line();
1193 assert!(line.contains("CPU: 50%"));
1194 assert!(!line.contains("Memory"));
1195 }
1196
1197 #[test]
1198 fn test_compiler_output_passthrough() {
1199 let display = BuildErrorDisplay::compilation_failed("cargo build")
1200 .worker("build1")
1201 .compiler_output("error[E0308]: mismatched types\n --> src/main.rs:5:5");
1202
1203 assert!(display.compiler_output.is_some());
1204 }
1205}