From cc9f4ab8f49e9f7c00a47a6fb9e1373e27e911ed Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Thu, 13 Mar 2025 13:00:17 -0700 Subject: [PATCH 1/2] Parse exceptions during reloads The reload summary ("Ok, 10 modules loaded") is still responsible for whether or not "All good!" is printed, so this still needs some work. We could just check if any exceptions (or errors?) are printed and mark it as a compilation failure if so? --- src/ghci/parse/ghc_message/exception.rs | 54 +++++++++++++++++++++++++ src/ghci/parse/ghc_message/mod.rs | 15 +++++++ tests/all_good.rs | 27 +++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 src/ghci/parse/ghc_message/exception.rs diff --git a/src/ghci/parse/ghc_message/exception.rs b/src/ghci/parse/ghc_message/exception.rs new file mode 100644 index 00000000..b09906da --- /dev/null +++ b/src/ghci/parse/ghc_message/exception.rs @@ -0,0 +1,54 @@ +use winnow::PResult; +use winnow::Parser; + +use crate::ghci::parse::lines::until_newline; + +use super::GhcMessage; + +/// Parse an "Exception" message like this: +/// +/// ```plain +/// *** Exception: /Users/.../dist-newstyle/ghc82733_tmp_1/ghc_tmp_34657.h: withFile: does not exist (No such file or directory) +/// ``` +pub fn exception(input: &mut &str) -> PResult { + let _ = "*** Exception: ".parse_next(input)?; + + let message = until_newline.parse_next(input)?; + + Ok(GhcMessage::Exception(message.to_owned())) +} + +#[cfg(test)] +mod tests { + use super::*; + + use indoc::indoc; + use pretty_assertions::assert_eq; + + #[test] + fn test_parse_exception() { + assert_eq!( + exception.parse("*** Exception: Uh oh!\n").unwrap(), + GhcMessage::Exception("Uh oh!".into()) + ); + + assert_eq!( + exception.parse("*** Exception: /Users/.../dist-newstyle/ghc82733_tmp_1/ghc_tmp_34657.h: withFile: does not exist (No such file or directory)\n").unwrap(), + GhcMessage::Exception("/Users/.../dist-newstyle/ghc82733_tmp_1/ghc_tmp_34657.h: withFile: does not exist (No such file or directory)".into()) + ); + + // Doesn't parse subsequent lines (even if they're relevant, unfortunately). + assert_eq!( + exception + .parse(indoc!( + " + *** Exception: puppy doggy + CallStack (from HasCallStack): + error, called at :3:1 in interactive:Ghci1 + " + )) + .unwrap(), + GhcMessage::Exception("puppy doggy".into()) + ); + } +} diff --git a/src/ghci/parse/ghc_message/mod.rs b/src/ghci/parse/ghc_message/mod.rs index 59733df9..6036a1df 100644 --- a/src/ghci/parse/ghc_message/mod.rs +++ b/src/ghci/parse/ghc_message/mod.rs @@ -44,6 +44,9 @@ use module_import_cycle_diagnostic::module_import_cycle_diagnostic; mod no_location_info_diagnostic; use no_location_info_diagnostic::no_location_info_diagnostic; +mod exception; +use exception::exception; + use super::rest_of_line; use super::CompilingModule; @@ -64,6 +67,15 @@ pub enum GhcMessage { /// Foo.hs:81:1: Warning: Defined but not used: `bar' /// ``` Diagnostic(GhcDiagnostic), + /// An exception while running a command or similar. + /// + /// These may be multiple lines, but there's no good way to determine where the message ends, + /// so we just parse the first line. + /// + /// ```text + /// *** Exception: /Users/.../dist-newstyle/ghc82733_tmp_1/ghc_tmp_34657.h: withFile: does not exist (No such file or directory) + /// ``` + Exception(String), /// A configuration file being loaded. /// /// ```text @@ -181,6 +193,7 @@ fn parse_messages_inner(input: &mut &str) -> PResult> { .map(Item::One), module_import_cycle_diagnostic.map(Item::Many), loaded_configuration.map(Item::One), + exception.map(Item::One), rest_of_line.map(|line| { tracing::debug!(line, "Ignoring GHC output line"); Item::Ignore @@ -290,6 +303,7 @@ mod tests { | 4 | example = "example" | ^^^^^^^^^ + *** Exception: /Users/.../dist-newstyle/ghc82733_tmp_1/ghc_tmp_34657.h: withFile: does not exist (No such file or directory) Failed, two modules loaded. "# )) @@ -322,6 +336,7 @@ mod tests { ].join("\n"), }, ), + GhcMessage::Exception("/Users/.../dist-newstyle/ghc82733_tmp_1/ghc_tmp_34657.h: withFile: does not exist (No such file or directory)".to_owned()), GhcMessage::Summary( CompilationSummary { result: CompilationResult::Err, diff --git a/tests/all_good.rs b/tests/all_good.rs index 395c7a8c..e2406e21 100644 --- a/tests/all_good.rs +++ b/tests/all_good.rs @@ -43,3 +43,30 @@ async fn can_detect_compilation_failure() { .await .unwrap(); } + +/// Test that `ghciwatch` can detect an `*** Exception` diagnostic. +/// +/// Regression test for DUX-3144. +#[test] +async fn can_detect_exception() { + let mut session = GhciWatchBuilder::new("tests/data/simple") + .with_args(["--after-reload-ghci", r#"error "oopsie daisy!""#]) + .start() + .await + .expect("ghciwatch starts"); + let module_path = session.path("src/MyModule.hs"); + + session.wait_until_ready().await.expect("ghciwatch loads"); + + session.fs().append(&module_path, "\n").await.unwrap(); + + session + .wait_for_log(BaseMatcher::compilation_failed()) + .await + .unwrap(); + + session + .wait_for_log(BaseMatcher::reload_completes().but_not(BaseMatcher::message("All good!"))) + .await + .unwrap(); +} From ab38af5b49de1145021d19e07e6c18002429a6ac Mon Sep 17 00:00:00 2001 From: Rebecca Turner Date: Tue, 27 May 2025 09:48:11 -0700 Subject: [PATCH 2/2] Reproducer doesn't work Recently I thought a failing pre-processor would cause this issue, but it looks like that doesn't work: ``` ghci> :reload src/MyModule.hs:1:1: error: `false' failed in phase `Haskell pre-processor'. (Exit code: 1) | 1 | {-# OPTIONS_GHC -F -pgmF false #-} | ^ Failed, no modules to be reloaded. ``` --- tests/all_good.rs | 11 +++++++---- tests/data/simple/src/MyModule.hs | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/tests/all_good.rs b/tests/all_good.rs index e2406e21..0e125832 100644 --- a/tests/all_good.rs +++ b/tests/all_good.rs @@ -50,15 +50,18 @@ async fn can_detect_compilation_failure() { #[test] async fn can_detect_exception() { let mut session = GhciWatchBuilder::new("tests/data/simple") - .with_args(["--after-reload-ghci", r#"error "oopsie daisy!""#]) .start() .await .expect("ghciwatch starts"); - let module_path = session.path("src/MyModule.hs"); - session.wait_until_ready().await.expect("ghciwatch loads"); - session.fs().append(&module_path, "\n").await.unwrap(); + let module_path = session.path("src/MyModule.hs"); + + session + .fs() + .prepend(&module_path, "{-# OPTIONS_GHC -F -pgmF false #-}\n") + .await + .unwrap(); session .wait_for_log(BaseMatcher::compilation_failed()) diff --git a/tests/data/simple/src/MyModule.hs b/tests/data/simple/src/MyModule.hs index 87a6839d..12cf7f21 100644 --- a/tests/data/simple/src/MyModule.hs +++ b/tests/data/simple/src/MyModule.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -F -pgmF false #-} + module MyModule (example) where example :: String