1use crate::DisplayMode;
23use utf8proj_core::{Project, RenderError, Renderer, Schedule, ScheduledTask};
24
25#[derive(Clone, Debug)]
27pub struct MermaidRenderer {
28 pub show_sections: bool,
30 pub show_critical: bool,
32 pub show_completion: bool,
34 pub date_format: String,
36 pub use_dependencies: bool,
38 pub exclude_weekends: bool,
40 pub display_mode: DisplayMode,
42 pub label_width: usize,
44}
45
46impl Default for MermaidRenderer {
47 fn default() -> Self {
48 Self {
49 show_sections: true,
50 show_critical: true,
51 show_completion: true,
52 date_format: "YYYY-MM-DD".into(),
53 use_dependencies: true,
54 exclude_weekends: false,
55 display_mode: DisplayMode::Name,
56 label_width: 40,
57 }
58 }
59}
60
61impl MermaidRenderer {
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn no_sections(mut self) -> Self {
68 self.show_sections = false;
69 self
70 }
71
72 pub fn no_critical(mut self) -> Self {
74 self.show_critical = false;
75 self
76 }
77
78 pub fn no_completion(mut self) -> Self {
80 self.show_completion = false;
81 self
82 }
83
84 pub fn absolute_dates(mut self) -> Self {
86 self.use_dependencies = false;
87 self
88 }
89
90 pub fn date_format(mut self, format: impl Into<String>) -> Self {
92 self.date_format = format.into();
93 self
94 }
95
96 pub fn exclude_weekends(mut self) -> Self {
98 self.exclude_weekends = true;
99 self
100 }
101
102 pub fn display_mode(mut self, mode: DisplayMode) -> Self {
104 self.display_mode = mode;
105 self
106 }
107
108 pub fn label_width(mut self, width: usize) -> Self {
110 self.label_width = width;
111 self
112 }
113
114 fn sanitize_name(name: &str) -> String {
116 name.replace(':', "-")
118 .replace(';', "-")
119 .replace('#', "")
120 .replace('\n', " ")
121 .replace('\r', "")
122 }
123
124 fn make_id(task_id: &str) -> String {
126 task_id
128 .chars()
129 .map(|c| {
130 if c.is_alphanumeric() || c == '_' {
131 c
132 } else {
133 '_'
134 }
135 })
136 .collect()
137 }
138
139 fn format_duration(task: &ScheduledTask) -> String {
141 let days = task.duration.as_days().ceil() as i64;
142 if days == 0 {
143 "0d".into()
144 } else {
145 format!("{}d", days)
146 }
147 }
148
149 fn get_modifiers(&self, task: &ScheduledTask, complete: Option<f32>) -> Vec<&'static str> {
151 let mut mods = Vec::new();
152
153 if task.duration.minutes == 0 {
155 mods.push("milestone");
156 }
157
158 if self.show_critical && task.is_critical {
160 mods.push("crit");
161 }
162
163 if self.show_completion {
165 if let Some(pct) = complete {
166 if pct >= 100.0 {
167 mods.push("done");
168 } else if pct > 0.0 {
169 mods.push("active");
170 }
171 }
172 }
173
174 mods
175 }
176}
177
178impl Renderer for MermaidRenderer {
179 type Output = String;
180
181 fn render(&self, project: &Project, schedule: &Schedule) -> Result<String, RenderError> {
182 if schedule.tasks.is_empty() {
183 return Err(RenderError::InvalidData("No tasks to render".into()));
184 }
185
186 let mut output = String::new();
187
188 output.push_str("gantt\n");
190 output.push_str(&format!(
191 " title {}\n",
192 Self::sanitize_name(&project.name)
193 ));
194 output.push_str(&format!(" dateFormat {}\n", self.date_format));
195
196 if self.exclude_weekends {
198 output.push_str(" excludes weekends\n");
199 }
200
201 output.push('\n');
202
203 let mut tasks: Vec<(&String, &ScheduledTask)> = schedule.tasks.iter().collect();
205 tasks.sort_by_key(|(_, t)| t.start);
206
207 let mut first_predecessor: std::collections::HashMap<String, String> =
209 std::collections::HashMap::new();
210 for task in &project.tasks {
211 self.collect_predecessors(task, &mut first_predecessor);
212 }
213
214 if self.show_sections {
216 let mut sections: std::collections::HashMap<String, Vec<(&String, &ScheduledTask)>> =
218 std::collections::HashMap::new();
219
220 for (task_id, scheduled) in &tasks {
221 let section = if task_id.contains('.') {
222 task_id.split('.').next().unwrap_or("Tasks").to_string()
223 } else {
224 "Tasks".to_string()
225 };
226 sections
227 .entry(section)
228 .or_default()
229 .push((task_id, scheduled));
230 }
231
232 let mut section_names: Vec<_> = sections.keys().cloned().collect();
234 section_names.sort();
235
236 for section_name in section_names {
237 if let Some(section_tasks) = sections.get(§ion_name) {
238 let leaf_section_id = section_name.rsplit('.').next().unwrap_or(§ion_name);
241 let display_name = project
242 .get_task(leaf_section_id)
243 .map(|t| t.name.clone())
244 .unwrap_or_else(|| section_name.clone());
245
246 output.push_str(&format!(
247 " section {}\n",
248 Self::sanitize_name(&display_name)
249 ));
250
251 for (task_id, scheduled) in section_tasks {
252 let line =
253 self.format_task_line(task_id, scheduled, project, &first_predecessor);
254 output.push_str(&format!(" {}\n", line));
255 }
256 output.push('\n');
257 }
258 }
259 } else {
260 for (task_id, scheduled) in &tasks {
262 let line = self.format_task_line(task_id, scheduled, project, &first_predecessor);
263 output.push_str(&format!(" {}\n", line));
264 }
265 }
266
267 Ok(output)
268 }
269}
270
271impl MermaidRenderer {
272 fn collect_predecessors(
274 &self,
275 task: &utf8proj_core::Task,
276 map: &mut std::collections::HashMap<String, String>,
277 ) {
278 if let Some(first_dep) = task.depends.first() {
279 map.insert(task.id.clone(), first_dep.predecessor.clone());
280 }
281 for child in &task.children {
282 self.collect_predecessors(child, map);
283 }
284 }
285
286 fn format_task_line(
288 &self,
289 task_id: &str,
290 scheduled: &ScheduledTask,
291 project: &Project,
292 first_predecessor: &std::collections::HashMap<String, String>,
293 ) -> String {
294 let leaf_id = task_id.rsplit('.').next().unwrap_or(task_id);
297 let task = project.get_task(leaf_id);
298 let name = task
299 .map(|t| t.name.clone())
300 .unwrap_or_else(|| task_id.to_string());
301 let complete = task.and_then(|t| t.complete);
302
303 let label = self
305 .display_mode
306 .format_label(task_id, &name, self.label_width);
307 let sanitized_name = Self::sanitize_name(&label);
308 let mermaid_id = Self::make_id(task_id);
309 let duration = Self::format_duration(scheduled);
310 let modifiers = self.get_modifiers(scheduled, complete);
311
312 let mut parts = Vec::new();
314
315 for m in &modifiers {
317 parts.push(m.to_string());
318 }
319
320 parts.push(mermaid_id.clone());
322
323 if self.use_dependencies {
325 if let Some(pred) = first_predecessor.get(task_id) {
326 parts.push(format!("after {}", Self::make_id(pred)));
327 } else {
328 parts.push(scheduled.start.format("%Y-%m-%d").to_string());
329 }
330 } else {
331 parts.push(scheduled.start.format("%Y-%m-%d").to_string());
332 }
333
334 parts.push(duration);
336
337 format!("{} :{}", sanitized_name, parts.join(", "))
338 }
339}
340
341#[cfg(test)]
342mod tests {
343 use super::*;
344 use chrono::NaiveDate;
345 use std::collections::HashMap;
346 use utf8proj_core::{Duration, Schedule, ScheduledTask, Task, TaskStatus};
347
348 fn create_test_project() -> Project {
349 let mut project = Project::new("Test Project");
350 project.start = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
351 project.tasks.push(
352 Task::new("design")
353 .name("Design Phase")
354 .effort(Duration::days(5)),
355 );
356 project.tasks.push(
357 Task::new("implement")
358 .name("Implementation")
359 .effort(Duration::days(10))
360 .depends_on("design"),
361 );
362 project.tasks.push(
363 Task::new("test")
364 .name("Testing")
365 .effort(Duration::days(3))
366 .depends_on("implement"),
367 );
368 project
369 }
370
371 fn create_test_schedule() -> Schedule {
372 let mut tasks = HashMap::new();
373
374 let start1 = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
375 let finish1 = NaiveDate::from_ymd_opt(2025, 1, 10).unwrap();
376 tasks.insert(
377 "design".to_string(),
378 ScheduledTask {
379 task_id: "design".to_string(),
380 start: start1,
381 finish: finish1,
382 duration: Duration::days(5),
383 assignments: vec![],
384 slack: Duration::zero(),
385 is_critical: true,
386 early_start: start1,
387 early_finish: finish1,
388 late_start: start1,
389 late_finish: finish1,
390 forecast_start: start1,
391 forecast_finish: finish1,
392 remaining_duration: Duration::days(5),
393 percent_complete: 0,
394 status: TaskStatus::NotStarted,
395 cost_range: None,
396 has_abstract_assignments: false,
397 baseline_start: start1,
398 baseline_finish: finish1,
399 start_variance_days: 0,
400 finish_variance_days: 0,
401 },
402 );
403
404 let start2 = NaiveDate::from_ymd_opt(2025, 1, 13).unwrap();
405 let finish2 = NaiveDate::from_ymd_opt(2025, 1, 24).unwrap();
406 tasks.insert(
407 "implement".to_string(),
408 ScheduledTask {
409 task_id: "implement".to_string(),
410 start: start2,
411 finish: finish2,
412 duration: Duration::days(10),
413 assignments: vec![],
414 slack: Duration::zero(),
415 is_critical: true,
416 early_start: start2,
417 early_finish: finish2,
418 late_start: start2,
419 late_finish: finish2,
420 forecast_start: start2,
421 forecast_finish: finish2,
422 remaining_duration: Duration::days(10),
423 percent_complete: 0,
424 status: TaskStatus::NotStarted,
425 cost_range: None,
426 has_abstract_assignments: false,
427 baseline_start: start2,
428 baseline_finish: finish2,
429 start_variance_days: 0,
430 finish_variance_days: 0,
431 },
432 );
433
434 let start3 = NaiveDate::from_ymd_opt(2025, 1, 27).unwrap();
435 let finish3 = NaiveDate::from_ymd_opt(2025, 1, 29).unwrap();
436 tasks.insert(
437 "test".to_string(),
438 ScheduledTask {
439 task_id: "test".to_string(),
440 start: start3,
441 finish: finish3,
442 duration: Duration::days(3),
443 assignments: vec![],
444 slack: Duration::zero(),
445 is_critical: true,
446 early_start: start3,
447 early_finish: finish3,
448 late_start: start3,
449 late_finish: finish3,
450 forecast_start: start3,
451 forecast_finish: finish3,
452 remaining_duration: Duration::days(3),
453 percent_complete: 0,
454 status: TaskStatus::NotStarted,
455 cost_range: None,
456 has_abstract_assignments: false,
457 baseline_start: start3,
458 baseline_finish: finish3,
459 start_variance_days: 0,
460 finish_variance_days: 0,
461 },
462 );
463
464 let project_end = NaiveDate::from_ymd_opt(2025, 1, 29).unwrap();
465 Schedule {
466 tasks,
467 critical_path: vec![
468 "design".to_string(),
469 "implement".to_string(),
470 "test".to_string(),
471 ],
472 project_duration: Duration::days(18),
473 project_end,
474 total_cost: None,
475 total_cost_range: None,
476 project_progress: 0,
477 project_baseline_finish: project_end,
478 project_forecast_finish: project_end,
479 project_variance_days: 0,
480 planned_value: 0,
481 earned_value: 0,
482 spi: 1.0,
483 }
484 }
485
486 #[test]
487 fn mermaid_renderer_creation() {
488 let renderer = MermaidRenderer::new();
489 assert!(renderer.show_sections);
490 assert!(renderer.show_critical);
491 assert_eq!(renderer.date_format, "YYYY-MM-DD");
492 }
493
494 #[test]
495 fn mermaid_renderer_with_options() {
496 let renderer = MermaidRenderer::new()
497 .no_sections()
498 .no_critical()
499 .absolute_dates()
500 .exclude_weekends();
501
502 assert!(!renderer.show_sections);
503 assert!(!renderer.show_critical);
504 assert!(!renderer.use_dependencies);
505 assert!(renderer.exclude_weekends);
506 }
507
508 #[test]
509 fn mermaid_produces_valid_output() {
510 let renderer = MermaidRenderer::new();
511 let project = create_test_project();
512 let schedule = create_test_schedule();
513
514 let result = renderer.render(&project, &schedule);
515 assert!(result.is_ok());
516
517 let output = result.unwrap();
518 assert!(output.starts_with("gantt\n"));
519 assert!(output.contains("title Test Project"));
520 assert!(output.contains("dateFormat YYYY-MM-DD"));
521 }
522
523 #[test]
524 fn mermaid_includes_critical_marker() {
525 let renderer = MermaidRenderer::new();
526 let project = create_test_project();
527 let schedule = create_test_schedule();
528
529 let output = renderer.render(&project, &schedule).unwrap();
530 assert!(output.contains("crit"));
531 }
532
533 #[test]
534 fn mermaid_uses_after_syntax() {
535 let renderer = MermaidRenderer::new();
536 let project = create_test_project();
537 let schedule = create_test_schedule();
538
539 let output = renderer.render(&project, &schedule).unwrap();
540 assert!(output.contains("after design"));
541 assert!(output.contains("after implement"));
542 }
543
544 #[test]
545 fn mermaid_absolute_dates_mode() {
546 let renderer = MermaidRenderer::new().absolute_dates();
547 let project = create_test_project();
548 let schedule = create_test_schedule();
549
550 let output = renderer.render(&project, &schedule).unwrap();
551 assert!(!output.contains("after "));
552 assert!(output.contains("2025-01-06"));
553 assert!(output.contains("2025-01-13"));
554 }
555
556 #[test]
557 fn mermaid_empty_schedule_fails() {
558 let renderer = MermaidRenderer::new();
559 let project = Project::new("Empty");
560 let project_end = NaiveDate::from_ymd_opt(2025, 1, 1).unwrap();
561 let schedule = Schedule {
562 tasks: HashMap::new(),
563 critical_path: vec![],
564 project_duration: Duration::zero(),
565 project_end,
566 total_cost: None,
567 total_cost_range: None,
568 project_progress: 0,
569 project_baseline_finish: project_end,
570 project_forecast_finish: project_end,
571 project_variance_days: 0,
572 planned_value: 0,
573 earned_value: 0,
574 spi: 1.0,
575 };
576
577 let result = renderer.render(&project, &schedule);
578 assert!(result.is_err());
579 }
580
581 #[test]
582 fn mermaid_sanitizes_special_chars() {
583 assert_eq!(
584 MermaidRenderer::sanitize_name("Task: Phase 1"),
585 "Task- Phase 1"
586 );
587 assert_eq!(MermaidRenderer::sanitize_name("Test;Task"), "Test-Task");
588 assert_eq!(MermaidRenderer::sanitize_name("Task #1"), "Task 1");
589 }
590
591 #[test]
592 fn mermaid_makes_valid_ids() {
593 assert_eq!(MermaidRenderer::make_id("task1"), "task1");
594 assert_eq!(MermaidRenderer::make_id("phase1.design"), "phase1_design");
595 assert_eq!(
596 MermaidRenderer::make_id("task-with-dashes"),
597 "task_with_dashes"
598 );
599 }
600
601 #[test]
602 fn mermaid_excludes_weekends() {
603 let renderer = MermaidRenderer::new().exclude_weekends();
604 let project = create_test_project();
605 let schedule = create_test_schedule();
606
607 let output = renderer.render(&project, &schedule).unwrap();
608 assert!(output.contains("excludes weekends"));
609 }
610
611 #[test]
612 fn mermaid_milestone_detection() {
613 let mut project = Project::new("Milestone Test");
614 project.start = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
615 project
616 .tasks
617 .push(Task::new("done").name("Project Complete").milestone());
618
619 let ms_date = NaiveDate::from_ymd_opt(2025, 1, 10).unwrap();
620 let mut tasks = HashMap::new();
621 tasks.insert(
622 "done".to_string(),
623 ScheduledTask {
624 task_id: "done".to_string(),
625 start: ms_date,
626 finish: ms_date,
627 duration: Duration::zero(),
628 assignments: vec![],
629 slack: Duration::zero(),
630 is_critical: true,
631 early_start: ms_date,
632 early_finish: ms_date,
633 late_start: ms_date,
634 late_finish: ms_date,
635 forecast_start: ms_date,
636 forecast_finish: ms_date,
637 remaining_duration: Duration::zero(),
638 percent_complete: 0,
639 status: TaskStatus::NotStarted,
640 cost_range: None,
641 has_abstract_assignments: false,
642 baseline_start: ms_date,
643 baseline_finish: ms_date,
644 start_variance_days: 0,
645 finish_variance_days: 0,
646 },
647 );
648
649 let project_end = NaiveDate::from_ymd_opt(2025, 1, 10).unwrap();
650 let schedule = Schedule {
651 tasks,
652 critical_path: vec!["done".to_string()],
653 project_duration: Duration::zero(),
654 project_end,
655 total_cost: None,
656 total_cost_range: None,
657 project_progress: 0,
658 project_baseline_finish: project_end,
659 project_forecast_finish: project_end,
660 project_variance_days: 0,
661 planned_value: 0,
662 earned_value: 0,
663 spi: 1.0,
664 };
665
666 let renderer = MermaidRenderer::new();
667 let output = renderer.render(&project, &schedule).unwrap();
668 assert!(output.contains("milestone"));
669 }
670
671 #[test]
672 fn mermaid_no_completion_option() {
673 let renderer = MermaidRenderer::new().no_completion();
674 assert!(!renderer.show_completion);
675 }
676
677 #[test]
678 fn mermaid_custom_date_format() {
679 let renderer = MermaidRenderer::new().date_format("DD-MM-YYYY");
680 assert_eq!(renderer.date_format, "DD-MM-YYYY");
681
682 let project = create_test_project();
683 let schedule = create_test_schedule();
684 let output = renderer.render(&project, &schedule).unwrap();
685 assert!(output.contains("dateFormat DD-MM-YYYY"));
686 }
687
688 #[test]
689 fn mermaid_no_sections_flat_list() {
690 let renderer = MermaidRenderer::new().no_sections();
691 let project = create_test_project();
692 let schedule = create_test_schedule();
693
694 let output = renderer.render(&project, &schedule).unwrap();
695 assert!(!output.contains("section "));
697 }
698
699 #[test]
700 fn mermaid_done_modifier_for_complete_task() {
701 let mut project = Project::new("Progress Test");
702 project.start = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
703 project.tasks.push(
704 Task::new("complete")
705 .name("Completed Task")
706 .effort(Duration::days(5))
707 .complete(100.0),
708 );
709
710 let start = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
711 let finish = NaiveDate::from_ymd_opt(2025, 1, 10).unwrap();
712 let mut tasks = HashMap::new();
713 tasks.insert(
714 "complete".to_string(),
715 ScheduledTask {
716 task_id: "complete".to_string(),
717 start,
718 finish,
719 duration: Duration::days(5),
720 assignments: vec![],
721 slack: Duration::zero(),
722 is_critical: true,
723 early_start: start,
724 early_finish: finish,
725 late_start: start,
726 late_finish: finish,
727 forecast_start: start,
728 forecast_finish: finish,
729 remaining_duration: Duration::zero(),
730 percent_complete: 100,
731 status: TaskStatus::Complete,
732 cost_range: None,
733 has_abstract_assignments: false,
734 baseline_start: start,
735 baseline_finish: finish,
736 start_variance_days: 0,
737 finish_variance_days: 0,
738 },
739 );
740
741 let schedule = Schedule {
742 tasks,
743 critical_path: vec!["complete".to_string()],
744 project_duration: Duration::days(5),
745 project_end: finish,
746 total_cost: None,
747 total_cost_range: None,
748 project_progress: 0,
749 project_baseline_finish: finish,
750 project_forecast_finish: finish,
751 project_variance_days: 0,
752 planned_value: 0,
753 earned_value: 0,
754 spi: 1.0,
755 };
756
757 let renderer = MermaidRenderer::new();
758 let output = renderer.render(&project, &schedule).unwrap();
759 assert!(output.contains("done"));
760 }
761
762 #[test]
763 fn mermaid_active_modifier_for_in_progress_task() {
764 let mut project = Project::new("Progress Test");
765 project.start = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
766 project.tasks.push(
767 Task::new("inprogress")
768 .name("In Progress Task")
769 .effort(Duration::days(10))
770 .complete(50.0),
771 );
772
773 let start = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
774 let finish = NaiveDate::from_ymd_opt(2025, 1, 17).unwrap();
775 let mut tasks = HashMap::new();
776 tasks.insert(
777 "inprogress".to_string(),
778 ScheduledTask {
779 task_id: "inprogress".to_string(),
780 start,
781 finish,
782 duration: Duration::days(10),
783 assignments: vec![],
784 slack: Duration::zero(),
785 is_critical: true,
786 early_start: start,
787 early_finish: finish,
788 late_start: start,
789 late_finish: finish,
790 forecast_start: start,
791 forecast_finish: finish,
792 remaining_duration: Duration::days(5),
793 percent_complete: 50,
794 status: TaskStatus::InProgress,
795 cost_range: None,
796 has_abstract_assignments: false,
797 baseline_start: start,
798 baseline_finish: finish,
799 start_variance_days: 0,
800 finish_variance_days: 0,
801 },
802 );
803
804 let schedule = Schedule {
805 tasks,
806 critical_path: vec!["inprogress".to_string()],
807 project_duration: Duration::days(10),
808 project_end: finish,
809 total_cost: None,
810 total_cost_range: None,
811 project_progress: 0,
812 project_baseline_finish: finish,
813 project_forecast_finish: finish,
814 project_variance_days: 0,
815 planned_value: 0,
816 earned_value: 0,
817 spi: 1.0,
818 };
819
820 let renderer = MermaidRenderer::new();
821 let output = renderer.render(&project, &schedule).unwrap();
822 assert!(output.contains("active"));
823 }
824
825 #[test]
826 fn mermaid_no_completion_hides_done_active() {
827 let mut project = Project::new("No Completion");
828 project.start = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
829 project.tasks.push(
830 Task::new("task")
831 .name("Task")
832 .effort(Duration::days(5))
833 .complete(100.0),
834 );
835
836 let start = NaiveDate::from_ymd_opt(2025, 1, 6).unwrap();
837 let finish = NaiveDate::from_ymd_opt(2025, 1, 10).unwrap();
838 let mut tasks = HashMap::new();
839 tasks.insert(
840 "task".to_string(),
841 ScheduledTask {
842 task_id: "task".to_string(),
843 start,
844 finish,
845 duration: Duration::days(5),
846 assignments: vec![],
847 slack: Duration::zero(),
848 is_critical: false,
849 early_start: start,
850 early_finish: finish,
851 late_start: start,
852 late_finish: finish,
853 forecast_start: start,
854 forecast_finish: finish,
855 remaining_duration: Duration::zero(),
856 percent_complete: 100,
857 status: TaskStatus::Complete,
858 cost_range: None,
859 has_abstract_assignments: false,
860 baseline_start: start,
861 baseline_finish: finish,
862 start_variance_days: 0,
863 finish_variance_days: 0,
864 },
865 );
866
867 let schedule = Schedule {
868 tasks,
869 critical_path: vec![],
870 project_duration: Duration::days(5),
871 project_end: finish,
872 total_cost: None,
873 total_cost_range: None,
874 project_progress: 0,
875 project_baseline_finish: finish,
876 project_forecast_finish: finish,
877 project_variance_days: 0,
878 planned_value: 0,
879 earned_value: 0,
880 spi: 1.0,
881 };
882
883 let renderer = MermaidRenderer::new().no_completion().no_critical();
884 let output = renderer.render(&project, &schedule).unwrap();
885 assert!(!output.contains("done"));
887 assert!(!output.contains("active"));
888 assert!(!output.contains("crit"));
889 }
890}