Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / language-server / src / feedback.rs
27 kB 792 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2023 The Gleam contributors 3 4use gleam_core::{Error, Warning, diagnostic::Diagnostic}; 5use std::collections::{HashMap, HashSet}; 6 7use camino::Utf8PathBuf; 8 9use super::engine::Compilation; 10 11#[derive(Debug, Default, PartialEq, Eq)] 12pub struct Feedback { 13 pub diagnostics: HashMap<Utf8PathBuf, Vec<Diagnostic>>, 14 pub messages: Vec<Diagnostic>, 15} 16 17impl Feedback { 18 /// Set the diagnostics for a file to an empty vector. This will overwrite 19 /// any existing diagnostics on the client. 20 pub fn unset_existing_diagnostics(&mut self, path: Utf8PathBuf) { 21 _ = self.diagnostics.insert(path, vec![]); 22 } 23 24 pub fn append_diagnostic(&mut self, path: Utf8PathBuf, diagnostic: Diagnostic) { 25 self.diagnostics.entry(path).or_default().push(diagnostic); 26 } 27 28 /// No feedback at all. 29 /// 30 pub fn none() -> Feedback { 31 Feedback::default() 32 } 33 34 /// Add all the content of another feedback to this feedback. 35 /// 36 pub fn append_feedback(&mut self, feedback: Feedback) { 37 for (path, diagnostics) in feedback.diagnostics { 38 // Any new diagnostics for a file will overwrite any existing ones. 39 _ = self.diagnostics.insert(path, diagnostics); 40 } 41 for diagnostic in feedback.messages { 42 self.append_message(diagnostic); 43 } 44 } 45 46 fn append_message(&mut self, diagnostic: Diagnostic) { 47 self.messages.push(diagnostic); 48 } 49} 50 51/// When an operation succeeds or fails we want to send diagnostics and 52/// messages to the client for displaying to the user. This object converts 53/// Gleam warnings, errors, etc to these feedback items. 54/// 55/// Gleam has incremental compilation so we cannot erase all previous 56/// diagnostics and replace each time new diagnostics are available; if a file 57/// has not been recompiled then any diagnostics it had previously are still 58/// valid and must not be erased. 59/// To do this we keep track of which files have diagnostics and only overwrite 60/// them if the file has been recompiled. 61/// 62#[derive(Debug, Default)] 63pub struct FeedbackBookKeeper { 64 files_with_warnings: HashSet<Utf8PathBuf>, 65 files_with_errors: HashSet<Utf8PathBuf>, 66 /// Skipped files need special handling: a compile will skip a file if it 67 /// depends on some other file that couldn't be compiled. 68 /// This means that skipped files will not have regular type errors, but a 69 /// single error telling the developer to go fix the wrong file they depend 70 /// on. 71 /// 72 /// However, we don't want those errors to always be displayed, it would be 73 /// quite annoying having to browse through them in the IDE while the only 74 /// error that really matters is the one in the wrong module they depend on. 75 /// But it's really important that this is displayed on files that are open 76 /// in the IDE so that a developer is not confused by the total lack of 77 /// errors and can go fix them. 78 /// 79 /// For each file skipped during compilation we keep track of that error 80 /// message and make sure to present it to the developer if the file is 81 /// open. 82 skipped_files_diagnostics: HashMap<Utf8PathBuf, Diagnostic>, 83 open_files: HashSet<Utf8PathBuf>, 84} 85 86impl FeedbackBookKeeper { 87 /// Send diagnostics for any warnings and remove any diagnostics for files 88 /// that have compiled without warnings. 89 /// 90 pub fn response(&mut self, compilation: Compilation, warnings: Vec<Warning>) -> Feedback { 91 let mut feedback = Feedback::default(); 92 93 if let Compilation::Yes(compiled_modules) = compilation { 94 // Any existing diagnostics for files that have been compiled are no 95 // longer valid so we set an empty vector of diagnostics for the files 96 // to erase their diagnostics. 97 for path in compiled_modules { 98 let has_existing_diagnostics = self.files_with_warnings.remove(&path); 99 if has_existing_diagnostics { 100 feedback.unset_existing_diagnostics(path); 101 } 102 } 103 104 // Compilation was attempted and there is no error (which there is not 105 // in this function) then it means that compilation has succeeded, so 106 // there should be no error diagnostics. 107 // We don't limit this to files that have been compiled as a previous 108 // cached version could be used instead of a recompile. 109 self.unset_errors(&mut feedback); 110 self.unset_skipped_files(&mut feedback); 111 } 112 113 for warning in warnings { 114 self.insert_warning(&mut feedback, warning); 115 } 116 117 feedback 118 } 119 120 fn unset_errors(&mut self, feedback: &mut Feedback) { 121 // TODO: avoid clobbering warnings. They should be preserved rather than 122 // removed with the errors here. We will need to store the warnings and 123 // re-send them. 124 for path in self.files_with_errors.drain() { 125 feedback.unset_existing_diagnostics(path); 126 } 127 } 128 129 fn unset_skipped_files(&mut self, feedback: &mut Feedback) { 130 for (path, _) in self.skipped_files_diagnostics.drain() { 131 feedback.unset_existing_diagnostics(path); 132 } 133 } 134 135 /// Compilation failed, boo! 136 /// 137 /// Send diagnostics for any warnings and remove any diagnostics for files 138 /// that have compiled without warnings, AND ALSO send diagnostics for the 139 /// error that caused compilation to fail. 140 /// 141 pub fn build_with_error( 142 &mut self, 143 error: Error, 144 compilation: Compilation, 145 warnings: Vec<Warning>, 146 ) -> Feedback { 147 let diagnostics = error.to_diagnostics(); 148 let mut feedback = self.response(compilation, warnings); 149 150 // A new error means that any existing errors are no longer valid. 151 // Unset them. 152 self.unset_skipped_files(&mut feedback); 153 self.unset_errors(&mut feedback); 154 155 for diagnostic in diagnostics { 156 match diagnostic.location.as_ref().map(|l| l.path.clone()) { 157 Some(path) => { 158 _ = self.files_with_errors.insert(path.clone()); 159 feedback.append_diagnostic(path, diagnostic); 160 } 161 162 None => { 163 feedback.append_message(diagnostic); 164 } 165 } 166 } 167 168 self.skipped_files_diagnostics = error.skipped_files_diagnostics(); 169 for open_file in self.open_files.iter() { 170 if let Some(diagnostic) = self.skipped_files_diagnostics.get(open_file) { 171 feedback.append_diagnostic(open_file.clone(), diagnostic.clone()); 172 } 173 } 174 175 feedback 176 } 177 178 pub fn error(&mut self, error: Error) -> Feedback { 179 self.build_with_error(error, Compilation::No, vec![]) 180 } 181 182 fn insert_warning(&mut self, feedback: &mut Feedback, warning: Warning) { 183 let diagnostic = warning.to_diagnostic(); 184 if let Some(path) = diagnostic.location.as_ref().map(|l| l.path.clone()) { 185 _ = self.files_with_warnings.insert(path.clone()); 186 feedback.append_diagnostic(path, diagnostic); 187 } 188 } 189 190 pub fn open_file(&mut self, file: Utf8PathBuf) -> Feedback { 191 let _ = self.open_files.insert(file.clone()); 192 let mut feedback = Feedback::none(); 193 if let Some(diagnostic) = self.skipped_files_diagnostics.get(&file) { 194 feedback.append_diagnostic(file, diagnostic.clone()); 195 } 196 feedback 197 } 198 199 pub fn close_file(&mut self, file: &Utf8PathBuf) -> Feedback { 200 let _ = self.open_files.remove(file); 201 let mut feedback = Feedback::none(); 202 if self.skipped_files_diagnostics.contains_key(file) { 203 feedback.unset_existing_diagnostics(file.clone()); 204 } 205 feedback 206 } 207} 208 209#[cfg(test)] 210mod tests { 211 212 use std::assert_eq; 213 214 use super::*; 215 use gleam_core::{ 216 ast::SrcSpan, 217 diagnostic::Level, 218 parse::error::{ParseError, ParseErrorType}, 219 type_, 220 }; 221 222 #[test] 223 fn feedback() { 224 let mut book_keeper = FeedbackBookKeeper::default(); 225 let file1 = Utf8PathBuf::from("src/file1.gleam"); 226 let file2 = Utf8PathBuf::from("src/file2.gleam"); 227 let file3 = Utf8PathBuf::from("src/file3.gleam"); 228 229 let warning1 = Warning::Type { 230 path: file1.clone(), 231 src: "src".into(), 232 warning: Box::new(type_::Warning::NoFieldsRecordUpdate { 233 location: SrcSpan::new(1, 2), 234 }), 235 }; 236 let warning2 = Warning::Type { 237 path: file2.clone(), 238 src: "src".into(), 239 warning: Box::new(type_::Warning::NoFieldsRecordUpdate { 240 location: SrcSpan::new(1, 2), 241 }), 242 }; 243 244 let feedback = book_keeper.response( 245 Compilation::Yes(vec![file1.clone()]), 246 vec![warning1.clone(), warning1.clone(), warning2.clone()], 247 ); 248 249 assert_eq!( 250 Feedback { 251 diagnostics: HashMap::from([ 252 ( 253 file1.clone(), 254 vec![warning1.to_diagnostic(), warning1.to_diagnostic(),] 255 ), 256 (file2.clone(), vec![warning2.to_diagnostic(),]) 257 ]), 258 messages: vec![], 259 }, 260 feedback 261 ); 262 263 let feedback = book_keeper.response( 264 Compilation::Yes(vec![file1.clone(), file2.clone(), file3]), 265 vec![], 266 ); 267 268 assert_eq!( 269 Feedback { 270 diagnostics: HashMap::from([ 271 // File 1 and 2 had diagnostics before so they have been unset 272 (file1, vec![]), 273 (file2, vec![]), 274 // File 3 had no diagnostics so does not need to to be unset 275 ]), 276 messages: vec![], 277 }, 278 feedback 279 ); 280 } 281 282 #[test] 283 fn locationless_error() { 284 // The failed method sets an additional messages for errors without a 285 // location. 286 287 let mut book_keeper = FeedbackBookKeeper::default(); 288 let file1 = Utf8PathBuf::from("src/file1.gleam"); 289 290 let warning1 = Warning::Type { 291 path: file1.clone(), 292 src: "src".into(), 293 warning: Box::new(type_::Warning::NoFieldsRecordUpdate { 294 location: SrcSpan::new(1, 2), 295 }), 296 }; 297 298 let locationless_error = Error::Gzip("Hello!".into()); 299 300 let feedback = book_keeper.build_with_error( 301 locationless_error.clone(), 302 Compilation::Yes(vec![]), 303 vec![warning1.clone()], 304 ); 305 306 assert_eq!( 307 Feedback { 308 diagnostics: HashMap::from([(file1, vec![warning1.to_diagnostic()])]), 309 messages: locationless_error.to_diagnostics(), 310 }, 311 feedback 312 ); 313 } 314 315 #[test] 316 fn error() { 317 // The failed method sets an additional diagnostic if the error has a 318 // location. 319 320 let mut book_keeper = FeedbackBookKeeper::default(); 321 let file1 = Utf8PathBuf::from("src/file1.gleam"); 322 let file3 = Utf8PathBuf::from("src/file2.gleam"); 323 324 let warning1 = Warning::Type { 325 path: file1.clone(), 326 src: "src".into(), 327 warning: Box::new(type_::Warning::NoFieldsRecordUpdate { 328 location: SrcSpan::new(1, 2), 329 }), 330 }; 331 let error = Error::Parse { 332 path: file3.clone(), 333 src: "blah".into(), 334 error: Box::new(ParseError { 335 error: ParseErrorType::ConcatPatternVariableLeftHandSide, 336 location: SrcSpan::new(1, 4), 337 }), 338 }; 339 340 let feedback = book_keeper.build_with_error( 341 error.clone(), 342 Compilation::Yes(vec![]), 343 vec![warning1.clone()], 344 ); 345 346 assert_eq!( 347 Feedback { 348 diagnostics: HashMap::from([ 349 (file1, vec![warning1.to_diagnostic()]), 350 (file3.clone(), error.to_diagnostics()), 351 ]), 352 messages: vec![], 353 }, 354 feedback 355 ); 356 357 // The error diagnostic should be removed if the file compiles later. 358 359 let feedback = book_keeper.response(Compilation::Yes(vec![file3.clone()]), vec![]); 360 361 assert_eq!( 362 Feedback { 363 diagnostics: HashMap::from([(file3, vec![])]), 364 messages: vec![], 365 }, 366 feedback 367 ); 368 } 369 370 // https://github.com/gleam-lang/gleam/issues/2093 371 #[test] 372 fn successful_compilation_removes_error_diagnostic() { 373 // It is possible for a compile error to be fixed but the module that 374 // had the error to not actually be recompiled. 375 // 376 // 1. File is OK 377 // 2. File is edited to an invalid state 378 // 3. A compile error is emitted 379 // 4. File is edited back to the earlier valid state 380 // 5. File is not recompiled as the cache from step 1 is still valid 381 // 382 // Because of this the compiled files iterator does not contain the 383 // file, so we need to make sure that the error is removed through other 384 // means, such as tracking which files have errors and removing them all 385 // when a successful compilation occurs. 386 387 let mut book_keeper = FeedbackBookKeeper::default(); 388 let file1 = Utf8PathBuf::from("src/file1.gleam"); 389 let file2 = Utf8PathBuf::from("src/file2.gleam"); 390 391 let error = Error::Parse { 392 path: file1.clone(), 393 src: "blah".into(), 394 error: Box::new(ParseError { 395 error: ParseErrorType::ConcatPatternVariableLeftHandSide, 396 location: SrcSpan::new(1, 4), 397 }), 398 }; 399 400 let feedback = 401 book_keeper.build_with_error(error.clone(), Compilation::Yes(vec![]), vec![]); 402 403 assert_eq!( 404 Feedback { 405 diagnostics: HashMap::from([(file1.clone(), error.to_diagnostics())]), 406 messages: vec![], 407 }, 408 feedback 409 ); 410 411 // The error diagnostic should be removed on a successful compilation, 412 // even though the file is not in the compiled files iterator. 413 414 let feedback = book_keeper.response(Compilation::Yes(vec![file2]), vec![]); 415 416 assert_eq!( 417 Feedback { 418 diagnostics: HashMap::from([(file1, vec![])]), 419 messages: vec![], 420 }, 421 feedback 422 ); 423 } 424 425 // https://github.com/gleam-lang/gleam/issues/2122 426 #[test] 427 fn second_failure_unsets_previous_error() { 428 let mut book_keeper = FeedbackBookKeeper::default(); 429 let file1 = Utf8PathBuf::from("src/file1.gleam"); 430 let file2 = Utf8PathBuf::from("src/file2.gleam"); 431 432 let error = |file: &camino::Utf8Path| Error::Parse { 433 path: file.to_path_buf(), 434 src: "blah".into(), 435 error: Box::new(ParseError { 436 error: ParseErrorType::ConcatPatternVariableLeftHandSide, 437 location: SrcSpan::new(1, 4), 438 }), 439 }; 440 441 let feedback = 442 book_keeper.build_with_error(error(&file1), Compilation::Yes(vec![]), vec![]); 443 444 assert_eq!( 445 Feedback { 446 diagnostics: HashMap::from([(file1.clone(), error(&file1).to_diagnostics())]), 447 messages: vec![], 448 }, 449 feedback 450 ); 451 452 let feedback = 453 book_keeper.build_with_error(error(&file2), Compilation::Yes(vec![]), vec![]); 454 455 assert_eq!( 456 Feedback { 457 diagnostics: HashMap::from([ 458 // Unset the previous error 459 (file1, vec![]), 460 // Set the new one 461 (file2.clone(), error(&file2).to_diagnostics()), 462 ]), 463 messages: vec![], 464 }, 465 feedback 466 ); 467 } 468 469 // https://github.com/gleam-lang/gleam/issues/2105 470 #[test] 471 fn successful_non_compilation_does_not_remove_error_diagnostic() { 472 let mut book_keeper = FeedbackBookKeeper::default(); 473 let file1 = Utf8PathBuf::from("src/file1.gleam"); 474 475 let error = Error::Parse { 476 path: file1.clone(), 477 src: "blah".into(), 478 error: Box::new(ParseError { 479 error: ParseErrorType::ConcatPatternVariableLeftHandSide, 480 location: SrcSpan::new(1, 4), 481 }), 482 }; 483 484 let feedback = 485 book_keeper.build_with_error(error.clone(), Compilation::Yes(vec![]), vec![]); 486 487 assert_eq!( 488 Feedback { 489 diagnostics: HashMap::from([(file1, error.to_diagnostics())]), 490 messages: vec![], 491 }, 492 feedback 493 ); 494 495 // The error diagnostic should not be removed, nothing has been 496 // successfully compiled. 497 498 let feedback = book_keeper.response(Compilation::No, vec![]); 499 500 assert_eq!( 501 Feedback { 502 diagnostics: HashMap::new(), 503 messages: vec![], 504 }, 505 feedback 506 ); 507 } 508 509 #[test] 510 fn append_feedback_new_file() { 511 let mut feedback = Feedback { 512 diagnostics: HashMap::from([( 513 Utf8PathBuf::from("src/file1.gleam"), 514 vec![Diagnostic { 515 location: None, 516 hint: None, 517 text: "Error 1".to_string(), 518 title: "Error 1".to_string(), 519 level: Level::Error, 520 }], 521 )]), 522 messages: vec![Diagnostic { 523 location: None, 524 hint: None, 525 text: "Error 2".to_string(), 526 title: "Error 2".to_string(), 527 level: Level::Error, 528 }], 529 }; 530 feedback.append_feedback(Feedback { 531 diagnostics: HashMap::from([( 532 Utf8PathBuf::from("src/file2.gleam"), 533 vec![Diagnostic { 534 location: None, 535 hint: None, 536 text: "Error 3".to_string(), 537 title: "Error 3".to_string(), 538 level: Level::Error, 539 }], 540 )]), 541 messages: vec![], 542 }); 543 assert_eq!( 544 feedback, 545 Feedback { 546 diagnostics: HashMap::from([ 547 ( 548 Utf8PathBuf::from("src/file1.gleam"), 549 vec![Diagnostic { 550 location: None, 551 hint: None, 552 text: "Error 1".to_string(), 553 title: "Error 1".to_string(), 554 level: Level::Error, 555 }], 556 ), 557 ( 558 Utf8PathBuf::from("src/file2.gleam"), 559 vec![Diagnostic { 560 location: None, 561 hint: None, 562 text: "Error 3".to_string(), 563 title: "Error 3".to_string(), 564 level: Level::Error, 565 }], 566 ), 567 ]), 568 messages: vec![Diagnostic { 569 location: None, 570 hint: None, 571 text: "Error 2".to_string(), 572 title: "Error 2".to_string(), 573 level: Level::Error, 574 },], 575 } 576 ); 577 } 578 579 #[test] 580 fn append_feedback_same_file() { 581 let mut feedback = Feedback { 582 diagnostics: HashMap::from([( 583 Utf8PathBuf::from("src/file1.gleam"), 584 vec![Diagnostic { 585 location: None, 586 hint: None, 587 text: "Error 1".to_string(), 588 title: "Error 1".to_string(), 589 level: Level::Error, 590 }], 591 )]), 592 messages: vec![Diagnostic { 593 location: None, 594 hint: None, 595 text: "Error 2".to_string(), 596 title: "Error 2".to_string(), 597 level: Level::Error, 598 }], 599 }; 600 feedback.append_feedback(Feedback { 601 diagnostics: HashMap::from([( 602 Utf8PathBuf::from("src/file1.gleam"), 603 vec![Diagnostic { 604 location: None, 605 hint: None, 606 text: "Error 3".to_string(), 607 title: "Error 3".to_string(), 608 level: Level::Error, 609 }], 610 )]), 611 messages: vec![], 612 }); 613 assert_eq!( 614 feedback, 615 Feedback { 616 diagnostics: HashMap::from([( 617 Utf8PathBuf::from("src/file1.gleam"), 618 vec![Diagnostic { 619 location: None, 620 hint: None, 621 text: "Error 3".to_string(), 622 title: "Error 3".to_string(), 623 level: Level::Error, 624 }], 625 ),]), 626 messages: vec![Diagnostic { 627 location: None, 628 hint: None, 629 text: "Error 2".to_string(), 630 title: "Error 2".to_string(), 631 level: Level::Error, 632 },], 633 } 634 ); 635 } 636 637 #[test] 638 fn append_feedback_new_message() { 639 let mut feedback = Feedback { 640 diagnostics: HashMap::from([( 641 Utf8PathBuf::from("src/file1.gleam"), 642 vec![Diagnostic { 643 location: None, 644 hint: None, 645 text: "Error 1".to_string(), 646 title: "Error 1".to_string(), 647 level: Level::Error, 648 }], 649 )]), 650 messages: vec![Diagnostic { 651 location: None, 652 hint: None, 653 text: "Error 2".to_string(), 654 title: "Error 2".to_string(), 655 level: Level::Error, 656 }], 657 }; 658 feedback.append_feedback(Feedback { 659 diagnostics: HashMap::from([]), 660 messages: vec![Diagnostic { 661 location: None, 662 hint: None, 663 text: "Error 3".to_string(), 664 title: "Error 3".to_string(), 665 level: Level::Error, 666 }], 667 }); 668 assert_eq!( 669 feedback, 670 Feedback { 671 diagnostics: HashMap::from([( 672 Utf8PathBuf::from("src/file1.gleam"), 673 vec![Diagnostic { 674 location: None, 675 hint: None, 676 text: "Error 1".to_string(), 677 title: "Error 1".to_string(), 678 level: Level::Error, 679 },], 680 ),]), 681 messages: vec![ 682 Diagnostic { 683 location: None, 684 hint: None, 685 text: "Error 2".to_string(), 686 title: "Error 2".to_string(), 687 level: Level::Error, 688 }, 689 Diagnostic { 690 location: None, 691 hint: None, 692 text: "Error 3".to_string(), 693 title: "Error 3".to_string(), 694 level: Level::Error, 695 } 696 ], 697 } 698 ); 699 } 700 701 #[test] 702 fn append_feedback_new_file_blank() { 703 let mut feedback = Feedback { 704 diagnostics: HashMap::from([( 705 Utf8PathBuf::from("src/file1.gleam"), 706 vec![Diagnostic { 707 location: None, 708 hint: None, 709 text: "Error 1".to_string(), 710 title: "Error 1".to_string(), 711 level: Level::Error, 712 }], 713 )]), 714 messages: vec![Diagnostic { 715 location: None, 716 hint: None, 717 text: "Error 2".to_string(), 718 title: "Error 2".to_string(), 719 level: Level::Error, 720 }], 721 }; 722 feedback.append_feedback(Feedback { 723 diagnostics: HashMap::from([(Utf8PathBuf::from("src/file2.gleam"), vec![])]), 724 messages: vec![], 725 }); 726 assert_eq!( 727 feedback, 728 Feedback { 729 diagnostics: HashMap::from([ 730 ( 731 Utf8PathBuf::from("src/file1.gleam"), 732 vec![Diagnostic { 733 location: None, 734 hint: None, 735 text: "Error 1".to_string(), 736 title: "Error 1".to_string(), 737 level: Level::Error, 738 },], 739 ), 740 (Utf8PathBuf::from("src/file2.gleam"), vec![],), 741 ]), 742 messages: vec![Diagnostic { 743 location: None, 744 hint: None, 745 text: "Error 2".to_string(), 746 title: "Error 2".to_string(), 747 level: Level::Error, 748 },], 749 } 750 ); 751 } 752 753 #[test] 754 fn append_feedback_existing_file_blank() { 755 let mut feedback = Feedback { 756 diagnostics: HashMap::from([( 757 Utf8PathBuf::from("src/file1.gleam"), 758 vec![Diagnostic { 759 location: None, 760 hint: None, 761 text: "Error 1".to_string(), 762 title: "Error 1".to_string(), 763 level: Level::Error, 764 }], 765 )]), 766 messages: vec![Diagnostic { 767 location: None, 768 hint: None, 769 text: "Error 2".to_string(), 770 title: "Error 2".to_string(), 771 level: Level::Error, 772 }], 773 }; 774 feedback.append_feedback(Feedback { 775 diagnostics: HashMap::from([(Utf8PathBuf::from("src/file1.gleam"), vec![])]), 776 messages: vec![], 777 }); 778 assert_eq!( 779 feedback, 780 Feedback { 781 diagnostics: HashMap::from([(Utf8PathBuf::from("src/file1.gleam"), vec![],),]), 782 messages: vec![Diagnostic { 783 location: None, 784 hint: None, 785 text: "Error 2".to_string(), 786 title: "Error 2".to_string(), 787 level: Level::Error, 788 },], 789 } 790 ); 791 } 792}