Skip to content

Commit

Permalink
feat(forge): run last changed test file in watch mode (#860)
Browse files Browse the repository at this point in the history
  • Loading branch information
mattsse authored Mar 7, 2022
1 parent 79e5a9a commit 39b6e39
Show file tree
Hide file tree
Showing 3 changed files with 87 additions and 16 deletions.
2 changes: 1 addition & 1 deletion cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ walkdir = "2.3.2"
solang-parser = "0.1.2"
similar = { version = "2.1.0", features = ["inline"] }
console = "0.15.0"
watchexec = "2.0.0-pre.10"
watchexec = "2.0.0-pre.11"

[dev-dependencies]
foundry-utils = { path = "./../utils", features = ["test"] }
Expand Down
55 changes: 41 additions & 14 deletions cli/src/cmd/watch.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
//! Watch mode support

use crate::cmd::{build::BuildArgs, test::TestArgs};
use crate::{
cmd::{build::BuildArgs, test::TestArgs},
utils::{self, FoundryPathExt},
};
use clap::Parser;
use regex::Regex;
use std::{convert::Infallible, ffi::OsStr, path::PathBuf, str::FromStr, sync::Arc};
use std::{convert::Infallible, path::PathBuf, str::FromStr, sync::Arc};
use watchexec::{
action::{Action, Outcome, PreSpawn},
command::Shell,
Expand All @@ -15,8 +18,6 @@ use watchexec::{
Watchexec,
};

use crate::utils;

/// Executes a [`Watchexec`] that listens for changes in the project's src dir and reruns `forge
/// build`
pub async fn watch_build(args: BuildArgs) -> eyre::Result<()> {
Expand Down Expand Up @@ -54,7 +55,7 @@ pub async fn watch_test(args: TestArgs) -> eyre::Result<()> {
runtime,
Arc::clone(&wx),
cmd,
has_conflicting_pattern_args,
WatchTestState { has_conflicting_pattern_args, last_test_files: Default::default() },
on_test,
);

Expand All @@ -65,27 +66,43 @@ pub async fn watch_test(args: TestArgs) -> eyre::Result<()> {
Ok(())
}

#[derive(Debug, Clone)]
struct WatchTestState {
/// marks whether the initial test args contains args that would conflict when adding a
/// match-path arg
has_conflicting_pattern_args: bool,
/// Tracks the last changed test files, if any so that if a non-test file was modified we run
/// this file instead *Note:* this is a vec, so we can also watch out for changes
/// introduced by `forge fmt`
last_test_files: Vec<String>,
}

/// The `on_action` hook for `forge test --watch`
fn on_test(action: OnActionState<bool>) {
fn on_test(action: OnActionState<WatchTestState>) {
let OnActionState { args, runtime, action, wx, cmd, other } = action;
let has_conflicting_pattern_args = other;
let WatchTestState { has_conflicting_pattern_args, last_test_files } = other;
if has_conflicting_pattern_args {
// can't set conflicting arguments
return
}

let mut cmd = cmd.clone();
// get changed files and update command accordingly
let sol_files: Vec<_> = action

let mut changed_sol_test_files: Vec<_> = action
.events
.iter()
.flat_map(|e| e.paths())
.filter(|(path, _)| path.extension() == Some(OsStr::new("sol")))
.filter(|(path, _)| path.is_sol_test())
.filter_map(|(path, _)| path.to_str())
.map(str::to_string)
.collect();

if sol_files.is_empty() {
return
if changed_sol_test_files.is_empty() {
if last_test_files.is_empty() {
return
}
// reuse the old test files if a non test file was changed
changed_sol_test_files = last_test_files;
}

// replace `--match-path` | `-mp` argument
Expand All @@ -95,7 +112,7 @@ fn on_test(action: OnActionState<bool>) {
}

// append `--match-path` regex
let re_str = format!("({})", sol_files.join("|"));
let re_str = format!("({})", changed_sol_test_files.join("|"));
if let Ok(re) = Regex::from_str(&re_str) {
let mut new_cmd = cmd.clone();
new_cmd.push("--match-path".to_string());
Expand All @@ -104,7 +121,17 @@ fn on_test(action: OnActionState<bool>) {
let mut config = runtime.clone();
config.command(new_cmd);
// re-register the action
on_action(args.clone(), config, wx, cmd, has_conflicting_pattern_args, on_test);
on_action(
args.clone(),
config,
wx,
cmd,
WatchTestState {
has_conflicting_pattern_args,
last_test_files: changed_sol_test_files,
},
on_test,
);
} else {
eprintln!("failed to parse new regex {}", re_str);
}
Expand Down
46 changes: 45 additions & 1 deletion cli/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{future::Future, str::FromStr, time::Duration};
use std::{future::Future, path::Path, str::FromStr, time::Duration};

use ethers::{solc::EvmVersion, types::U256};
#[cfg(feature = "sputnik-evm")]
Expand All @@ -19,6 +19,36 @@ pub(crate) const VERSION_MESSAGE: &str = concat!(
")"
);

/// Useful extensions to [`std::path::Path`].
pub trait FoundryPathExt {
/// Returns true if the [`Path`] ends with `.t.sol`
fn is_sol_test(&self) -> bool;

/// Returns true if the [`Path`] has a `sol` extension
fn is_sol(&self) -> bool;

/// Returns true if the [`Path`] has a `yul` extension
fn is_yul(&self) -> bool;
}

impl<T: AsRef<Path>> FoundryPathExt for T {
fn is_sol_test(&self) -> bool {
self.as_ref()
.file_name()
.and_then(|s| s.to_str())
.map(|s| s.ends_with(".t.sol"))
.unwrap_or_default()
}

fn is_sol(&self) -> bool {
self.as_ref().extension() == Some(std::ffi::OsStr::new("sol"))
}

fn is_yul(&self) -> bool {
self.as_ref().extension() == Some(std::ffi::OsStr::new("yul"))
}
}

/// Initializes a tracing Subscriber for logging
#[allow(dead_code)]
pub fn subscriber() {
Expand Down Expand Up @@ -115,3 +145,17 @@ macro_rules! p_println {
}}
}
pub(crate) use p_println;

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn foundry_path_ext_works() {
let p = Path::new("contracts/MyTest.t.sol");
assert!(p.is_sol_test());
assert!(p.is_sol());
let p = Path::new("contracts/Greeter.sol");
assert!(!p.is_sol_test());
}
}

0 comments on commit 39b6e39

Please sign in to comment.