diff --git a/README.md b/README.md index 2446a0e2..9e762712 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ when source files change. - GHCi output is displayed to the user as soon as it's printed. - Ghciwatch can handle new modules, removed modules, or moved modules without a - hitch, so you don't need to manually restart it. + hitch - A variety of [lifecycle hooks](https://mercurytechnologies.github.io/ghciwatch/lifecycle-hooks.html) let you run Haskell code or shell commands on a variety of events. diff --git a/docs/introduction.md b/docs/introduction.md index 36502dc9..a9c4e579 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -9,6 +9,7 @@ when source files change. - GHCi output is displayed to the user as soon as it's printed. - Ghciwatch can handle new modules, removed modules, or moved modules without a + hitch - A variety of [lifecycle hooks](lifecycle-hooks.md) let you run Haskell code or shell commands on a variety of events. - Run a test suite with [`--test-ghci @@ -20,7 +21,6 @@ when source files change. - [Custom globs](cli.md#--reload-glob) can be supplied to reload or restart the GHCi session when non-Haskell files (like templates or database schema definitions) change. - hitch, so you don't need to manually restart it. - Ghciwatch can [clear the screen between reloads](cli.md#--clear). - Compilation errors can be written to a file with [`--error-file`](cli.md#--error-file), for compatibility with [ghcid's][ghcid] `--outputfile` option. diff --git a/src/cli.rs b/src/cli.rs index d3d37110..4d76eeaf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -55,15 +55,15 @@ use crate::normal_path::NormalPath; override_usage = "ghciwatch [--command SHELL_COMMAND] [--watch PATH] [OPTIONS ...]" )] pub struct Opts { - /// A shell command which starts a `ghci` REPL, e.g. `ghci` or `cabal v2-repl` or similar. + /// A shell command which starts a GHCi REPL, e.g. `ghci` or `cabal v2-repl` or similar. /// - /// This is used to launch the underlying `ghci` session that `ghciwatch` controls. + /// This is used to launch the underlying GHCi session that `ghciwatch` controls. /// /// May contain quoted arguments which will be parsed in a `sh`-like manner. #[arg(long, value_name = "SHELL_COMMAND")] pub command: Option, - /// A Haskell source file to load into a `ghci` REPL. + /// A Haskell source file to load into a GHCi REPL. /// /// Shortcut for `--command 'ghci PATH'`. Conflicts with `--command`. #[arg(value_name = "FILE", conflicts_with = "command")] @@ -154,7 +154,7 @@ pub struct WatchOpts { #[arg(long = "watch", value_name = "PATH")] pub paths: Vec, - /// Reload the `ghci` session when paths matching this glob change. + /// Reload the GHCi session when paths matching this glob change. /// /// By default, only changes to Haskell source files trigger reloads. If you'd like to exclude /// some files from that, you can add an ignore glob here, like `!src/my-special-dir/**/*.hs`. @@ -169,13 +169,9 @@ pub struct WatchOpts { #[arg(long = "reload-glob")] pub reload_globs: Vec, - /// Restart the `ghci` session when paths matching this glob change. + /// Restart the GHCi session when paths matching this glob change. /// - /// By default, only changes to `.cabal` or `.ghci` files or Haskell source files being - /// moved/removed will trigger restarts. - /// - /// Due to [a `ghci` bug][1], the `ghci` session must be restarted when Haskell modules are removed - /// or renamed. + /// By default, only changes to `.cabal` or `.ghci` files will trigger restarts. /// /// See `--reload-globs` for more details. /// diff --git a/src/ghci/mod.rs b/src/ghci/mod.rs index 8aca877c..5a1a6b44 100644 --- a/src/ghci/mod.rs +++ b/src/ghci/mod.rs @@ -379,6 +379,7 @@ impl Ghci { let mut needs_restart = Vec::new(); let mut needs_reload = Vec::new(); let mut needs_add = Vec::new(); + let mut needs_remove = Vec::new(); for event in events { let path = event.as_path(); let path = self.relative_path(path)?; @@ -395,7 +396,7 @@ impl Ghci { ); // Don't restart if we've explicitly ignored this path in a glob. - if (!restart_match.is_ignore() + if !restart_match.is_ignore() // Restart on `.cabal` and `.ghci` files. && (path .extension() @@ -406,31 +407,21 @@ impl Ghci { .map(|name| name == ".ghci") .unwrap_or(false) // Restart on explicit restart globs. - || restart_match.is_whitelist())) - // Even if we've explicitly ignored this path in a glob, `ghci` can't cope with - // removed modules, so we need to restart when modules are removed or renamed. - // - // See: https://gitlab.haskell.org/ghc/ghc/-/issues/11596 - // - // TODO: I should investigate if `:unadd` works for some classes of removed - // modules. - || (matches!(event, FileEvent::Remove(_)) - && path_is_haskell_source_file - && self.targets.contains_source_path(&path)) + || restart_match.is_whitelist()) { // Restart for this path. tracing::debug!(%path, "Needs restart"); needs_restart.push(path); - } else if reload_match.is_whitelist() { - // Extra extensions are always reloaded, never added. - tracing::debug!(%path, "Needs reload"); - needs_reload.push(path); - } else if !reload_match.is_ignore() - // Don't reload if we've explicitly ignored this path in a glob. - // Otherwise, reload when Haskell files are modified. - && matches!(event, FileEvent::Modify(_)) + } else if reload_match.is_ignore() { + // Ignoring this path, continue. + } else if matches!(event, FileEvent::Remove(_)) && path_is_haskell_source_file + && self.targets.contains_source_path(&path) { + tracing::debug!(%path, "Needs remove"); + needs_remove.push(path); + } else if matches!(event, FileEvent::Modify(_)) && path_is_haskell_source_file { + // Otherwise, reload when Haskell files are modified. if self.targets.contains_source_path(&path) { // We can `:reload` paths in the target set. tracing::debug!(%path, "Needs reload"); @@ -440,6 +431,10 @@ impl Ghci { tracing::debug!(%path, "Needs add"); needs_add.push(path); } + } else if reload_match.is_whitelist() { + // Extra extensions are always reloaded, never added. + tracing::debug!(%path, "Needs reload"); + needs_reload.push(path); } } @@ -447,6 +442,7 @@ impl Ghci { needs_restart, needs_reload, needs_add, + needs_remove, }) } @@ -480,20 +476,26 @@ impl Ghci { let mut log = CompilationLog::default(); - if actions.needs_add_or_reload() { + if actions.needs_modify() { self.opts.clear(); self.run_hooks(LifecycleEvent::Reload(hooks::When::Before), &mut log) .await?; } + if !actions.needs_remove.is_empty() { + tracing::info!( + "Removing modules from ghci:\n{}", + format_bulleted_list(&actions.needs_remove) + ); + self.remove_modules(&actions.needs_remove, &mut log).await?; + } + if !actions.needs_add.is_empty() { tracing::info!( "Adding modules to ghci:\n{}", format_bulleted_list(&actions.needs_add) ); - for path in &actions.needs_add { - self.add_module(path, &mut log).await?; - } + self.add_modules(&actions.needs_add, &mut log).await?; } if !actions.needs_reload.is_empty() { @@ -506,7 +508,7 @@ impl Ghci { .await?; } - if actions.needs_add_or_reload() { + if actions.needs_modify() { self.finish_compilation( start_instant, &mut log, @@ -641,6 +643,21 @@ impl Ghci { Ok(()) } + /// Remove all `eval_commands` for the given paths. + #[instrument(skip_all, level = "debug")] + async fn clear_eval_commands_for_paths( + &mut self, + paths: impl IntoIterator>, + ) { + if !self.opts.enable_eval { + return; + } + + for path in paths { + self.eval_commands.remove(path.borrow()); + } + } + /// Read and parse eval commands from the given `path`. #[instrument(level = "trace")] async fn parse_eval_commands(path: &Utf8Path) -> miette::Result> { @@ -653,29 +670,25 @@ impl Ghci { Ok(commands) } - /// `:add` a module to the `ghci` session by path. - /// - /// Optionally returns a compilation result. + /// `:add` a module or modules to the `ghci` session by path. #[instrument(skip(self), level = "debug")] - async fn add_module( + async fn add_modules( &mut self, - path: &NormalPath, + paths: &[NormalPath], log: &mut CompilationLog, ) -> miette::Result<()> { - if self.targets.contains_source_path(path.absolute()) { - tracing::debug!(%path, "Skipping `:add`ing already-loaded path"); - return Ok(()); - } + let modules = self.targets.format_modules(&self.search_paths, paths)?; self.stdin - .add_module(&mut self.stdout, path.relative(), log) + .add_modules(&mut self.stdout, &modules, log) .await?; - self.targets - .insert_source_path(path.clone(), TargetKind::Path); + for path in paths { + self.targets + .insert_source_path(path.clone(), TargetKind::Path); + } - self.refresh_eval_commands_for_paths(std::iter::once(path)) - .await?; + self.refresh_eval_commands_for_paths(paths).await?; Ok(()) } @@ -683,8 +696,6 @@ impl Ghci { /// `:add *` a module to the `ghci` session by path. /// /// This forces it to be interpreted. - /// - /// Optionally returns a compilation result. #[instrument(skip(self), level = "debug")] async fn interpret_module( &mut self, @@ -707,6 +718,31 @@ impl Ghci { Ok(()) } + /// `:unadd` a module or modules from the `ghci` session by path. + #[instrument(skip(self), level = "debug")] + async fn remove_modules( + &mut self, + paths: &[NormalPath], + log: &mut CompilationLog, + ) -> miette::Result<()> { + // Each `:unadd` implicitly reloads as well, so we have to `:unadd` all the modules in a + // single command so that GHCi doesn't try to load a bunch of removed modules after each + // one. + let modules = self.targets.format_modules(&self.search_paths, paths)?; + + self.stdin + .remove_modules(&mut self.stdout, &modules, log) + .await?; + + for path in paths { + self.targets.remove_source_path(path); + self.clear_eval_commands_for_paths(std::iter::once(path)) + .await; + } + + Ok(()) + } + /// Stop this `ghci` session and cancel the async tasks associated with it. #[instrument(skip_all, level = "debug")] async fn stop(&mut self) -> miette::Result<()> { @@ -851,12 +887,14 @@ struct ReloadActions { needs_reload: Vec, /// Paths to modules which need an `:add`. needs_add: Vec, + /// Paths to modules which need an `:unadd`. + needs_remove: Vec, } impl ReloadActions { - /// Do any modules need to be added or reloaded? - fn needs_add_or_reload(&self) -> bool { - !self.needs_add.is_empty() || !self.needs_reload.is_empty() + /// Do any modules need to be added, removed, or reloaded? + fn needs_modify(&self) -> bool { + !self.needs_add.is_empty() || !self.needs_reload.is_empty() || !self.needs_remove.is_empty() } /// Is a session restart needed? @@ -868,7 +906,7 @@ impl ReloadActions { fn kind(&self) -> GhciReloadKind { if self.needs_restart() { GhciReloadKind::Restart - } else if self.needs_add_or_reload() { + } else if self.needs_modify() { GhciReloadKind::Reload } else { GhciReloadKind::None @@ -881,7 +919,7 @@ impl ReloadActions { pub enum GhciReloadKind { /// Noop. No actions needed. None, - /// Reload and/or add modules. Can be interrupted. + /// Reload, add, and/or remove modules. Can be interrupted. Reload, /// Restart the whole session. Cannot be interrupted. Restart, diff --git a/src/ghci/parse/module_set.rs b/src/ghci/parse/module_set.rs index bd32169b..cc25ff1f 100644 --- a/src/ghci/parse/module_set.rs +++ b/src/ghci/parse/module_set.rs @@ -2,6 +2,7 @@ use std::borrow::Borrow; use std::cmp::Eq; use std::collections::hash_map::Keys; use std::collections::HashMap; +use std::fmt::Display; use std::hash::Hash; use std::path::Path; @@ -61,6 +62,17 @@ impl ModuleSet { } } + /// Remove a source path from this module set. + /// + /// Returns the target's kind, if it was present in the set. + pub fn remove_source_path

(&mut self, path: &P) -> Option + where + NormalPath: Borrow

, + P: Hash + Eq + ?Sized, + { + self.modules.remove(path) + } + /// Get the name used to refer to the given module path when importing it. /// /// If the module isn't imported, a path will be returned. @@ -99,6 +111,24 @@ impl ModuleSet { } } + /// Format modules for adding or removing from a GHCi session. + /// + /// See [`ModuleSet::module_import_name`]. + pub fn format_modules( + &self, + show_paths: &ShowPaths, + modules: &[NormalPath], + ) -> miette::Result { + modules + .iter() + .map(|path| { + self.module_import_name(show_paths, path) + .map(|module| module.name) + }) + .collect::, _>>() + .map(|modules| modules.join(" ")) + } + /// Iterate over the source paths in this module set. pub fn iter(&self) -> Keys<'_, NormalPath, TargetKind> { self.modules.keys() @@ -106,6 +136,7 @@ impl ModuleSet { } /// Information about a module to be imported into a `ghci` session. +#[derive(Debug, Clone)] pub struct ImportInfo { /// The name to refer to the module by. /// @@ -117,3 +148,9 @@ pub struct ImportInfo { /// Whether the module is already loaded in the `ghci` session. pub loaded: bool, } + +impl Display for ImportInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) + } +} diff --git a/src/ghci/stdin.rs b/src/ghci/stdin.rs index dad957a2..ce8d84e3 100644 --- a/src/ghci/stdin.rs +++ b/src/ghci/stdin.rs @@ -1,4 +1,3 @@ -use camino::Utf8Path; use miette::Context; use miette::IntoDiagnostic; use tokio::io::AsyncWriteExt; @@ -104,10 +103,10 @@ impl GhciStdin { } #[instrument(skip(self, stdout), level = "debug")] - pub async fn add_module( + pub async fn add_modules( &mut self, stdout: &mut GhciStdout, - path: &Utf8Path, + modules: &str, log: &mut CompilationLog, ) -> miette::Result<()> { // We use `:add` because `:load` unloads all previously loaded modules: @@ -117,7 +116,18 @@ impl GhciStdin { // > to unload all the currently loaded modules and bindings. // // https://downloads.haskell.org/ghc/latest/docs/users_guide/ghci.html#ghci-cmd-:load - self.write_line(stdout, &format!(":add {path}\n"), log) + self.write_line(stdout, &format!(":add {modules}\n"), log) + .await + } + + #[instrument(skip(self, stdout), level = "debug")] + pub async fn remove_modules( + &mut self, + stdout: &mut GhciStdout, + modules: &str, + log: &mut CompilationLog, + ) -> miette::Result<()> { + self.write_line(stdout, &format!(":unadd {modules}\n"), log) .await } diff --git a/src/hooks.rs b/src/hooks.rs index e4a20848..e6c66c65 100644 --- a/src/hooks.rs +++ b/src/hooks.rs @@ -69,7 +69,7 @@ impl LifecycleEvent { ), LifecycleEvent::Startup(_) => indoc!( " - Startup hooks run when `ghci` is started (at `ghciwatch` startup and after `ghci` restarts). + Startup hooks run when GHCi is started (at `ghciwatch` startup and after GHCi restarts). " ), LifecycleEvent::Reload(_) => indoc!( @@ -79,10 +79,7 @@ impl LifecycleEvent { ), LifecycleEvent::Restart(_) => indoc!( " - Due to [a `ghci` bug][1], the `ghci` session must be restarted when Haskell modules - are removed or renamed. - - [1]: https://gitlab.haskell.org/ghc/ghc/-/issues/11596 + The GHCi session must be restarted when `.cabal` or `.ghci` files are modified. " ), }.trim_end_matches('\n') diff --git a/test-harness/src/ghciwatch.rs b/test-harness/src/ghciwatch.rs index 38a0da24..8c5b2f46 100644 --- a/test-harness/src/ghciwatch.rs +++ b/test-harness/src/ghciwatch.rs @@ -303,6 +303,12 @@ impl GhciWatch { .args([ "--command", &repl_command, + "--watch", + "src", + "--watch", + "package.yaml", + "--restart-glob", + "**/package.yaml", "--before-startup-shell", "hpack --force .", "--log-filter", diff --git a/test-harness/src/matcher/base_matcher.rs b/test-harness/src/matcher/base_matcher.rs index 95d2acc1..9edbdae8 100644 --- a/test-harness/src/matcher/base_matcher.rs +++ b/test-harness/src/matcher/base_matcher.rs @@ -174,6 +174,11 @@ impl BaseMatcher { pub fn ghci_add() -> Self { Self::message("^Adding modules to ghci:\n") } + + /// Match when `ghci` removes modules. + pub fn ghci_remove() -> Self { + Self::message("^Removing modules from ghci:\n") + } } impl Display for BaseMatcher { diff --git a/tests/clear.rs b/tests/clear.rs index 2fa6c18e..85b99254 100644 --- a/tests/clear.rs +++ b/tests/clear.rs @@ -43,19 +43,12 @@ async fn clears_on_reload_and_restart() { .await .unwrap(); - { - // Rename the module and fix the module name to match the new path. - let module_path = session.path("src/MyModule.hs"); - let new_path = session.path("src/MyCoolModule.hs"); - session.fs_mut().disable_load_bearing_sleep(); - session.fs().rename(&module_path, &new_path).await.unwrap(); - session - .fs() - .replace(&new_path, "module MyModule", "module MyCoolModule") - .await - .unwrap(); - session.fs_mut().reset_load_bearing_sleep(); - } + // Modify the `package.yaml` to trigger a restart. + session + .fs() + .append(session.path("package.yaml"), "\n") + .await + .unwrap(); session.wait_for_log("Clearing the screen").await.unwrap(); session diff --git a/tests/globs.rs b/tests/globs.rs index 6cfecd87..ccd90e3e 100644 --- a/tests/globs.rs +++ b/tests/globs.rs @@ -163,14 +163,14 @@ async fn can_ignore_restart_paths() { .expect("ghciwatch doesn't restart when ignored globs are changed"); } -/// Test that `ghciwatch` restarts when a Haskell module is removed, even if a `--restart-glob` -/// explicitly ignores the path. -/// -/// This is needed to work around a `ghci` bug: https://gitlab.haskell.org/ghc/ghc/-/issues/11596 +/// Ghciwatch can ignore when a file is removed. #[test] -async fn can_restart_on_module_change_even_if_ignored() { +async fn can_ignore_removal() { let mut session = GhciWatchBuilder::new("tests/data/simple") - .with_args(["--restart-glob", "!**/*.hs"]) + .before_start(|project_root| async move { + Fs::new().touch(project_root.join("my-model.db")).await + }) + .with_args(["--reload-glob", "!**/*.db", "--watch", "."]) .start() .await .expect("ghciwatch starts"); @@ -182,12 +182,41 @@ async fn can_restart_on_module_change_even_if_ignored() { session .fs() - .remove(session.path("src/MyModule.hs")) + .remove(session.path("my-model.db")) .await .unwrap(); session - .wait_until_restart() + .wait_for_log(BaseMatcher::reload_completes().but_not(BaseMatcher::ghci_remove())) + .await + .expect("Ghciwatch ignores removed files"); +} + +/// Ghciwatch can whitelist reloads for removed files. +#[test] +async fn can_whitelist_removal() { + let mut session = GhciWatchBuilder::new("tests/data/simple") + .before_start(|project_root| async move { + Fs::new().touch(project_root.join("my-model.db")).await + }) + .with_args(["--reload-glob", "**/*.db", "--watch", "."]) + .start() + .await + .expect("ghciwatch starts"); + + session + .wait_until_ready() + .await + .expect("ghciwatch loads ghci"); + + session + .fs() + .remove(session.path("my-model.db")) + .await + .unwrap(); + + session + .wait_for_log(BaseMatcher::reload_completes().but_not(BaseMatcher::ghci_remove())) .await - .expect("ghciwatch restarts when Haskell files are removed"); + .expect("Ghciwatch ignores removed files"); } diff --git a/tests/hooks.rs b/tests/hooks.rs index dee9c15d..1329d277 100644 --- a/tests/hooks.rs +++ b/tests/hooks.rs @@ -96,9 +96,10 @@ async fn can_run_hooks() { ghci_hook(&mut session, "after-reload", "1").await; ghci_hook(&mut session, "after-reload", "2").await; + // Modify the `package.yaml` to trigger a restart. session .fs() - .remove(session.path("src/MyModule.hs")) + .append(session.path("package.yaml"), "\n") .await .unwrap(); @@ -219,9 +220,23 @@ async fn hooks_can_observe_error_log() { .await .unwrap(); - // Rename the module. - let new_path = session.path("src/MyCoolModule.hs"); - session.fs().rename(module_path, new_path).await.unwrap(); + { + session.fs_mut().disable_load_bearing_sleep(); + // Rename the module. + // This generates an error message we can observe in the error log, but it doesn't restart + // the GHCi session so we need to touch the `.cabal` file for that... + let new_path = session.path("src/MyCoolModule.hs"); + session.fs().rename(module_path, new_path).await.unwrap(); + + // Modify the `package.yaml` to trigger a restart. + session + .fs() + .append(session.path("package.yaml"), "\n") + .await + .unwrap(); + + session.fs_mut().reset_load_bearing_sleep(); + } session .wait_for_log( diff --git a/tests/remove.rs b/tests/remove.rs new file mode 100644 index 00000000..fedb4944 --- /dev/null +++ b/tests/remove.rs @@ -0,0 +1,40 @@ +use test_harness::test; +use test_harness::BaseMatcher; +use test_harness::GhciWatch; + +#[test] +async fn can_remove_multiple_modules_at_once() { + let mut session = GhciWatch::new("tests/data/simple") + .await + .expect("ghciwatch starts"); + session + .wait_until_ready() + .await + .expect("ghciwatch loads ghci"); + + session.fs_mut().disable_load_bearing_sleep(); + session + .fs() + .remove(session.path("src/MyLib.hs")) + .await + .unwrap(); + session + .fs() + .remove(session.path("src/MyModule.hs")) + .await + .unwrap(); + session.fs_mut().reset_load_bearing_sleep(); + + session + .wait_for_log(BaseMatcher::ghci_remove()) + .await + .expect("ghciwatch reloads on changes"); + session + .wait_for_log(BaseMatcher::compilation_succeeded()) + .await + .expect("ghciwatch reloads successfully"); + session + .wait_for_log(BaseMatcher::reload_completes()) + .await + .expect("ghciwatch finishes reloading"); +} diff --git a/tests/rename.rs b/tests/rename.rs index 567d0bab..bc4b5762 100644 --- a/tests/rename.rs +++ b/tests/rename.rs @@ -1,6 +1,7 @@ use test_harness::test; use test_harness::BaseMatcher; use test_harness::GhciWatch; +use test_harness::Matcher; /// Test that `ghciwatch` can restart correctly when modules are removed and added (i.e., renamed) /// at the same time. @@ -23,12 +24,14 @@ async fn can_compile_renamed_module() { .unwrap(); session - .wait_until_restart() + .wait_for_log(BaseMatcher::ghci_add().and(BaseMatcher::ghci_remove())) .await - .expect("ghciwatch restarts on module move"); + .expect("ghciwatch adds and removes modules on module move"); + // Weirdly GHCi is fine with modules that don't match the file name as long as you specify the + // module by path and not by name. session - .wait_for_log(BaseMatcher::compilation_failed()) + .wait_for_log(BaseMatcher::compilation_succeeded()) .await .unwrap(); diff --git a/tests/restart.rs b/tests/restart.rs deleted file mode 100644 index e7e39b56..00000000 --- a/tests/restart.rs +++ /dev/null @@ -1,64 +0,0 @@ -use indoc::indoc; - -use test_harness::test; -use test_harness::BaseMatcher; -use test_harness::GhciWatch; - -/// Test that `ghciwatch` can restart `ghci` after a module is moved. -#[test] -async fn can_restart_after_module_move() { - let mut session = GhciWatch::new("tests/data/simple") - .await - .expect("ghciwatch starts"); - session - .wait_until_ready() - .await - .expect("ghciwatch loads ghci"); - - let module_path = session.path("src/My/Module.hs"); - session - .fs() - .write( - &module_path, - indoc!( - "module My.Module (myIdent) where - myIdent :: () - myIdent = () - " - ), - ) - .await - .unwrap(); - session - .wait_until_add() - .await - .expect("ghciwatch loads new modules"); - - { - // Rename the module and fix the module name to match the new path. - let new_path = session.path("src/My/CoolModule.hs"); - session.fs_mut().disable_load_bearing_sleep(); - session.fs().rename(&module_path, &new_path).await.unwrap(); - session - .fs() - .replace(&new_path, "module My.Module", "module My.CoolModule") - .await - .unwrap(); - session.fs_mut().reset_load_bearing_sleep(); - } - - session - .wait_until_restart() - .await - .expect("ghciwatch restarts ghci"); - - session - .wait_for_log(BaseMatcher::module_compiling("My.CoolModule")) - .await - .unwrap(); - - session - .wait_for_log(BaseMatcher::compilation_succeeded()) - .await - .unwrap(); -}