Skip to content

Commit

Permalink
More lifecycle hooks (#92)
Browse files Browse the repository at this point in the history
This adds support for multiple test commands (`--test-ghci`) and
before/after reload/restart hooks: `--before-reload-ghci`,
`--after-reload-ghci`, `--before-restart-ghci`, and
`--after-restart-ghci`. We may also consider before/after hooks for eval
commands.
  • Loading branch information
9999years authored Sep 25, 2023
1 parent df0878f commit 839af18
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 46 deletions.
72 changes: 56 additions & 16 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,6 @@ pub struct Opts {
#[arg(long, value_name = "SHELL_COMMAND")]
pub command: Option<ClonableCommand>,

/// A `ghci` command which runs tests, like `TestMain.testMain`. If given, this command will be
/// run after reloads.
#[arg(long, value_name = "GHCI_COMMAND")]
pub test_ghci: Option<GhciCommand>,

/// Shell commands to run before starting or restarting `ghci`.
///
/// This can be used to regenerate `.cabal` files with `hpack`.
#[arg(long, value_name = "SHELL_COMMAND")]
pub before_startup_shell: Vec<ClonableCommand>,

/// `ghci` commands to run on startup. Use `:set args ...` in combination with `--test` to set
/// the command-line arguments for tests.
#[arg(long, value_name = "GHCI_COMMAND")]
pub after_startup_ghci: Vec<GhciCommand>,

/// A file to write compilation errors to. This is analogous to `ghcid.txt`.
#[arg(long)]
pub errors: Option<Utf8PathBuf>,
Expand All @@ -52,6 +36,10 @@ pub struct Opts {
#[arg(long)]
pub enable_eval: bool,

/// Lifecycle hooks and commands to run at various points.
#[command(flatten)]
pub hooks: HookOpts,

/// Options to modify file watching.
#[command(flatten)]
pub watch: WatchOpts,
Expand Down Expand Up @@ -132,6 +120,58 @@ pub struct LoggingOpts {
pub log_json: Option<Utf8PathBuf>,
}

/// Lifecycle hooks.
///
/// These are commands (mostly `ghci` commands) to run at various points in the `ghcid-ng`
/// lifecycle.
#[derive(Debug, Clone, clap::Args)]
#[clap(next_help_heading = "Lifecycle hooks")]
pub struct HookOpts {
/// `ghci` commands which runs tests, like `TestMain.testMain`. If given, these commands will be
/// run after reloads.
#[arg(long, value_name = "GHCI_COMMAND")]
pub test_ghci: Vec<GhciCommand>,

/// Shell commands to run before starting or restarting `ghci`.
///
/// This can be used to regenerate `.cabal` files with `hpack`.
#[arg(long, value_name = "SHELL_COMMAND")]
pub before_startup_shell: Vec<ClonableCommand>,

/// `ghci` commands to run on startup. Use `:set args ...` in combination with `--test` to set
/// the command-line arguments for tests.
#[arg(long, value_name = "GHCI_COMMAND")]
pub after_startup_ghci: Vec<GhciCommand>,

/// `ghci` commands to run before reloading `ghci`.
///
/// These are run when modules are change on disk; this does not necessarily correspond to a
/// `:reload` command.
#[arg(long, value_name = "GHCI_COMMAND")]
pub before_reload_ghci: Vec<GhciCommand>,

/// `ghci` commands to run after reloading `ghci`.
#[arg(long, value_name = "GHCI_COMMAND")]
pub after_reload_ghci: Vec<GhciCommand>,

/// `ghci` commands to run before restarting `ghci`.
///
/// See `--after-restart-ghci` for more details.
#[arg(long, value_name = "GHCI_COMMAND")]
pub before_restart_ghci: Vec<GhciCommand>,

/// `ghci` commands to run after restarting `ghci`.
///
/// `ghci` cannot reload after files are deleted due to a bug, so `ghcid-ng` has to restart the
/// underlying `ghci` session when this happens. Note that the `--before-restart-ghci` and
/// `--after-restart-ghci` commands will therefore run in different `ghci` sessions without
/// shared context.
///
/// See: https://gitlab.haskell.org/ghc/ghc/-/issues/9648
#[arg(long, value_name = "GHCI_COMMAND")]
pub after_restart_ghci: Vec<GhciCommand>,
}

impl Opts {
/// Perform late initialization of the command-line arguments. If `init` isn't called before
/// the arguments are used, the behavior is undefined.
Expand Down
9 changes: 9 additions & 0 deletions src/ghci/ghci_command.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::fmt::Debug;
use std::fmt::Display;
use std::ops::Deref;

/// A `ghci` command.
///
Expand Down Expand Up @@ -44,3 +45,11 @@ impl AsRef<str> for GhciCommand {
&self.0
}
}

impl Deref for GhciCommand {
type Target = str;

fn deref(&self) -> &Self::Target {
&self.0
}
}
39 changes: 27 additions & 12 deletions src/ghci/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ pub use ghci_command::GhciCommand;

use crate::aho_corasick::AhoCorasickExt;
use crate::buffers::LINE_BUFFER_CAPACITY;
use crate::cli::HookOpts;
use crate::cli::Opts;
use crate::command;
use crate::command::ClonableCommand;
Expand Down Expand Up @@ -81,14 +82,10 @@ pub struct GhciOpts {
pub command: ClonableCommand,
/// A path to write `ghci` errors to.
pub error_path: Option<Utf8PathBuf>,
/// Shell commands to run before starting or restarting `ghci`.
pub before_startup_shell: Vec<ClonableCommand>,
/// `ghci` commands to run after starting or restarting `ghci`.
pub after_startup_ghci: Vec<GhciCommand>,
/// `ghci` command which runs tests.
pub test_ghci: Option<GhciCommand>,
/// Enable running eval commands in files.
pub enable_eval: bool,
/// Lifecycle hooks, mostly `ghci` commands to run at certain points.
pub hooks: HookOpts,
}

impl GhciOpts {
Expand All @@ -107,10 +104,8 @@ impl GhciOpts {
Ok(Self {
command,
error_path: opts.errors.clone(),
before_startup_shell: opts.before_startup_shell.clone(),
after_startup_ghci: opts.after_startup_ghci.clone(),
test_ghci: opts.test_ghci.clone(),
enable_eval: opts.enable_eval,
hooks: opts.hooks.clone(),
})
}
}
Expand Down Expand Up @@ -165,7 +160,7 @@ impl Ghci {
{
let span = tracing::debug_span!("before_startup_shell");
let _enter = span.enter();
for command in &opts.before_startup_shell {
for command in &opts.hooks.before_startup_shell {
let program = &command.program;
let mut command = command.as_tokio();
let command_formatted = command::format(&command);
Expand Down Expand Up @@ -262,7 +257,7 @@ impl Ghci {

// Perform start-of-session initialization.
ret.stdin
.initialize(&mut ret.stdout, &ret.opts.after_startup_ghci)
.initialize(&mut ret.stdout, &ret.opts.hooks.after_startup_ghci)
.await?;

// Sync up for any prompts.
Expand Down Expand Up @@ -337,9 +332,24 @@ impl Ghci {
"Restarting ghci due to deleted/moved modules:\n{}",
format_bulleted_list(&actions.needs_restart)
);
for command in &self.opts.hooks.before_restart_ghci {
tracing::info!(%command, "Running before-restart command");
self.stdin.run_command(&mut self.stdout, command).await?;
}
self.stop().await?;
let new = Self::new(self.opts.clone()).await?;
let _ = std::mem::replace(self, new);
for command in &self.opts.hooks.after_restart_ghci {
tracing::info!(%command, "Running after-restart command");
self.stdin.run_command(&mut self.stdout, command).await?;
}
}

if actions.needs_add_or_reload() {
for command in &self.opts.hooks.before_reload_ghci {
tracing::info!(%command, "Running before-reload command");
self.stdin.run_command(&mut self.stdout, command).await?;
}
}

let mut compilation_failed = false;
Expand Down Expand Up @@ -371,6 +381,11 @@ impl Ghci {
}

if actions.needs_add_or_reload() {
for command in &self.opts.hooks.after_reload_ghci {
tracing::info!(%command, "Running after-reload command");
self.stdin.run_command(&mut self.stdout, command).await?;
}

if compilation_failed {
tracing::debug!("Compilation failed, skipping running tests.");
} else {
Expand Down Expand Up @@ -399,7 +414,7 @@ impl Ghci {
#[instrument(skip_all, level = "debug")]
pub async fn test(&mut self) -> miette::Result<()> {
self.stdin
.test(&mut self.stdout, self.opts.test_ghci.clone())
.test(&mut self.stdout, &self.opts.hooks.test_ghci)
.await?;
Ok(())
}
Expand Down
52 changes: 34 additions & 18 deletions src/ghci/stdin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ impl GhciStdin {
stdout.prompt(None).await
}

/// Run a [`GhciCommand`].
///
/// The command may be multiple lines.
#[instrument(skip(self, stdout), level = "debug")]
pub async fn run_command(
&mut self,
stdout: &mut GhciStdout,
command: &GhciCommand,
) -> miette::Result<Vec<GhcMessage>> {
let mut ret = Vec::new();

for line in command.lines() {
self.stdin
.write_all(line.as_bytes())
.await
.into_diagnostic()?;
self.stdin.write_all(b"\n").await.into_diagnostic()?;
ret.extend(stdout.prompt(None).await?);
}

Ok(ret)
}

#[instrument(skip(self, stdout), name = "stdin_initialize", level = "debug")]
pub async fn initialize(
&mut self,
Expand All @@ -64,8 +87,8 @@ impl GhciStdin {
.await?;

for command in setup_commands {
tracing::debug!(%command, "Running user intialization command");
self.write_line(stdout, &format!("{command}\n")).await?;
tracing::debug!(%command, "Running after-startup command");
self.run_command(stdout, command).await?;
}

Ok(())
Expand All @@ -81,18 +104,18 @@ impl GhciStdin {
pub async fn test(
&mut self,
stdout: &mut GhciStdout,
test_command: Option<GhciCommand>,
test_commands: &[GhciCommand],
) -> miette::Result<()> {
if let Some(test_command) = test_command {
self.set_mode(stdout, Mode::Testing).await?;
if test_commands.is_empty() {
tracing::debug!("No test command provided, not running tests");
}
self.set_mode(stdout, Mode::Testing).await?;
for test_command in test_commands {
tracing::debug!(command = %test_command, "Running user test command");
tracing::info!("Running tests");
tracing::info!("Running tests: {test_command}");
let start_time = Instant::now();
self.write_line(stdout, &format!("{test_command}\n"))
.await?;
self.run_command(stdout, test_command).await?;
tracing::info!("Finished running tests in {:.2?}", start_time.elapsed());
} else {
tracing::debug!("No test command provided, not running tests");
}

Ok(())
Expand Down Expand Up @@ -179,14 +202,7 @@ impl GhciStdin {
.into_diagnostic()?;
stdout.prompt(None).await?;

for line in command.as_ref().lines() {
self.stdin
.write_all(line.as_bytes())
.await
.into_diagnostic()?;
self.stdin.write_all(b"\n").await.into_diagnostic()?;
stdout.prompt(None).await?;
}
self.run_command(stdout, command).await?;

self.stdin
.write_all(format!(":module - *{module}\n").as_bytes())
Expand Down
Loading

0 comments on commit 839af18

Please sign in to comment.