This commit is contained in:
Félix Saparelli 2022-01-22 17:40:09 +13:00
parent 089122821d
commit 093b443dbc
9 changed files with 542 additions and 532 deletions

View File

@ -1,12 +1,21 @@
root = true root = true
[*] [*]
indent_style = space indent_style = tab
indent_size = 4 indent_size = 4
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.yml] [tests/snapshots/*]
indent_style = space
trim_trailing_whitespace = false
[*.{md,ronn}]
indent_style = space
indent_size = 4
[*.{cff,yml}]
indent_size = 2 indent_size = 2
indent_style = space

1
.rustfmt.toml Normal file
View File

@ -0,0 +1 @@
hard_tabs = true

View File

@ -2,11 +2,11 @@ use clap::{App, AppSettings, Arg, ArgMatches, ErrorKind, SubCommand};
use std::{env, process}; use std::{env, process};
pub fn parse() -> ArgMatches<'static> { pub fn parse() -> ArgMatches<'static> {
let footnote = "You can use the `-- command` style instead, note you'll need to use full commands, it won't prefix `cargo` for you.\n\nBy default, your entire project is watched, except for the target/ and .git/ folders, and your .ignore and .gitignore files are used to filter paths.".to_owned(); let footnote = "You can use the `-- command` style instead, note you'll need to use full commands, it won't prefix `cargo` for you.\n\nBy default, your entire project is watched, except for the target/ and .git/ folders, and your .ignore and .gitignore files are used to filter paths.".to_owned();
#[cfg(windows)] let footnote = format!("{}\n\nOn Windows, patterns given to -i have forward slashes (/) automatically converted to backward ones (\\) to ease command portability.", footnote); #[cfg(windows)] let footnote = format!("{}\n\nOn Windows, patterns given to -i have forward slashes (/) automatically converted to backward ones (\\) to ease command portability.", footnote);
let mut app = App::new(env!("CARGO_PKG_NAME")) let mut app = App::new(env!("CARGO_PKG_NAME"))
.bin_name("cargo") .bin_name("cargo")
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
.help_message("") .help_message("")
@ -207,32 +207,32 @@ pub fn parse() -> ArgMatches<'static> {
.after_help(footnote.as_str()), .after_help(footnote.as_str()),
); );
// Allow invocation of cargo-watch with both `cargo-watch watch ARGS` // Allow invocation of cargo-watch with both `cargo-watch watch ARGS`
// (as invoked by cargo) and `cargo-watch ARGS`. // (as invoked by cargo) and `cargo-watch ARGS`.
let mut args: Vec<String> = env::args().collect(); let mut args: Vec<String> = env::args().collect();
args.insert(1, "watch".into()); args.insert(1, "watch".into());
let matches = match app.get_matches_from_safe_borrow(args) { let matches = match app.get_matches_from_safe_borrow(args) {
Ok(matches) => matches, Ok(matches) => matches,
Err(err) => { Err(err) => {
match err.kind { match err.kind {
ErrorKind::HelpDisplayed => { ErrorKind::HelpDisplayed => {
println!("{}", err); println!("{}", err);
process::exit(0); process::exit(0);
} }
ErrorKind::VersionDisplayed => { ErrorKind::VersionDisplayed => {
// Unlike HelpDisplayed, VersionDisplayed emits the output // Unlike HelpDisplayed, VersionDisplayed emits the output
// by itself (clap-rs/clap#1390). It also does so without a // by itself (clap-rs/clap#1390). It also does so without a
// trailing newline, so we print one ourselves. // trailing newline, so we print one ourselves.
println!(); println!();
process::exit(0); process::exit(0);
} }
_ => app.get_matches(), _ => app.get_matches(),
} }
} }
}; };
matches.subcommand.unwrap().matches matches.subcommand.unwrap().matches
} }

View File

@ -8,43 +8,43 @@ mod root;
mod watch; mod watch;
fn main() -> Result<()> { fn main() -> Result<()> {
let matches = args::parse(); let matches = args::parse();
let debug = matches.is_present("log:debug"); let debug = matches.is_present("log:debug");
let info = matches.is_present("log:info"); let info = matches.is_present("log:info");
let quiet = matches.is_present("log:quiet"); let quiet = matches.is_present("log:quiet");
let testing = matches.is_present("once"); let testing = matches.is_present("once");
stderrlog::new() stderrlog::new()
.quiet(quiet) .quiet(quiet)
.show_module_names(debug) .show_module_names(debug)
.verbosity(if debug { .verbosity(if debug {
3 3
} else if info { } else if info {
2 2
} else { } else {
1 1
}) })
.timestamp(if testing { .timestamp(if testing {
Timestamp::Off Timestamp::Off
} else { } else {
Timestamp::Millisecond Timestamp::Millisecond
}) })
.init() .init()
.unwrap(); .unwrap();
root::change_dir( root::change_dir(
matches matches
.value_of("workdir") .value_of("workdir")
.map(Utf8PathBuf::from) .map(Utf8PathBuf::from)
.unwrap_or_else(root::project_root), .unwrap_or_else(root::project_root),
); );
if let Some(b) = matches.value_of("rust-backtrace") { if let Some(b) = matches.value_of("rust-backtrace") {
std::env::set_var("RUST_BACKTRACE", b); std::env::set_var("RUST_BACKTRACE", b);
} }
let opts = options::get_options(&matches); let opts = options::get_options(&matches);
let handler = watch::CwHandler::new(opts, quiet, matches.is_present("notif"))?; let handler = watch::CwHandler::new(opts, quiet, matches.is_present("notif"))?;
watch(&handler) watch(&handler)
} }

View File

@ -3,260 +3,260 @@ use std::{env, path::MAIN_SEPARATOR, time::Duration};
use clap::{value_t, values_t, ArgMatches}; use clap::{value_t, values_t, ArgMatches};
use log::{debug, warn}; use log::{debug, warn};
use watchexec::{ use watchexec::{
config::{Config, ConfigBuilder}, config::{Config, ConfigBuilder},
run::OnBusyUpdate, run::OnBusyUpdate,
Shell, Shell,
}; };
pub fn set_commands(builder: &mut ConfigBuilder, matches: &ArgMatches) { pub fn set_commands(builder: &mut ConfigBuilder, matches: &ArgMatches) {
let mut commands: Vec<String> = Vec::new(); let mut commands: Vec<String> = Vec::new();
// --features are injected just after applicable cargo subcommands // --features are injected just after applicable cargo subcommands
// and before the remaining arguments // and before the remaining arguments
let features = value_t!(matches, "features", String).ok(); let features = value_t!(matches, "features", String).ok();
if matches.is_present("cmd:trail") { if matches.is_present("cmd:trail") {
debug!("trailing command is present, ignore all other command options"); debug!("trailing command is present, ignore all other command options");
commands = vec![values_t!(matches, "cmd:trail", String) commands = vec![values_t!(matches, "cmd:trail", String)
.unwrap_or_else(|e| e.exit()) .unwrap_or_else(|e| e.exit())
.into_iter() .into_iter()
.map(|arg| shell_escape::escape(arg.into())) .map(|arg| shell_escape::escape(arg.into()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" ")]; .join(" ")];
} else { } else {
let command_order = env::args().filter_map(|arg| match arg.as_str() { let command_order = env::args().filter_map(|arg| match arg.as_str() {
"-x" | "--exec" => Some("cargo"), "-x" | "--exec" => Some("cargo"),
"-s" | "--shell" => Some("shell"), "-s" | "--shell" => Some("shell"),
_ => None, _ => None,
}); });
let mut cargos = if matches.is_present("cmd:cargo") { let mut cargos = if matches.is_present("cmd:cargo") {
values_t!(matches, "cmd:cargo", String).unwrap_or_else(|e| e.exit()) values_t!(matches, "cmd:cargo", String).unwrap_or_else(|e| e.exit())
} else { } else {
Vec::new() Vec::new()
} }
.into_iter(); .into_iter();
let mut shells = if matches.is_present("cmd:shell") { let mut shells = if matches.is_present("cmd:shell") {
values_t!(matches, "cmd:shell", String).unwrap_or_else(|e| e.exit()) values_t!(matches, "cmd:shell", String).unwrap_or_else(|e| e.exit())
} else { } else {
Vec::new() Vec::new()
} }
.into_iter(); .into_iter();
for c in command_order { for c in command_order {
match c { match c {
"cargo" => { "cargo" => {
commands.push(cargo_command( commands.push(cargo_command(
cargos cargos
.next() .next()
.expect("Argument-order mismatch, this is a bug"), .expect("Argument-order mismatch, this is a bug"),
&features, &features,
)); ));
} }
"shell" => { "shell" => {
commands.push( commands.push(
shells shells
.next() .next()
.expect("Argument-order mismatch, this is a bug"), .expect("Argument-order mismatch, this is a bug"),
); );
} }
_ => {} _ => {}
} }
} }
} }
// Default to `cargo check` // Default to `cargo check`
if commands.is_empty() { if commands.is_empty() {
let mut cmd: String = "cargo check".into(); let mut cmd: String = "cargo check".into();
if let Some(features) = features.as_ref() { if let Some(features) = features.as_ref() {
cmd.push_str(" --features "); cmd.push_str(" --features ");
cmd.push_str(features); cmd.push_str(features);
} }
commands.push(cmd); commands.push(cmd);
} }
debug!("Commands: {:?}", commands); debug!("Commands: {:?}", commands);
builder.cmd(commands); builder.cmd(commands);
} }
fn cargo_command(cargo: String, features: &Option<String>) -> String { fn cargo_command(cargo: String, features: &Option<String>) -> String {
let mut cmd = String::from("cargo "); let mut cmd = String::from("cargo ");
let cargo = cargo.trim_start(); let cargo = cargo.trim_start();
if let Some(features) = features.as_ref() { if let Some(features) = features.as_ref() {
if cargo.starts_with('b') if cargo.starts_with('b')
|| cargo.starts_with("check") || cargo.starts_with("check")
|| cargo.starts_with("doc") || cargo.starts_with("doc")
|| cargo.starts_with('r') || cargo.starts_with('r')
|| cargo.starts_with("test") || cargo.starts_with("test")
|| cargo.starts_with("install") || cargo.starts_with("install")
{ {
// Split command into first word and the arguments // Split command into first word and the arguments
let word_boundary = cargo let word_boundary = cargo
.find(|c: char| c.is_whitespace()) .find(|c: char| c.is_whitespace())
.unwrap_or_else(|| cargo.len()); .unwrap_or_else(|| cargo.len());
// Find returns the byte index, and split_at takes a byte offset. // Find returns the byte index, and split_at takes a byte offset.
// This means the splitting is unicode-safe. // This means the splitting is unicode-safe.
let (subcommand, args) = cargo.split_at(word_boundary); let (subcommand, args) = cargo.split_at(word_boundary);
cmd.push_str(subcommand); cmd.push_str(subcommand);
cmd.push_str(" --features "); cmd.push_str(" --features ");
cmd.push_str(features); cmd.push_str(features);
cmd.push(' '); cmd.push(' ');
cmd.push_str(args); cmd.push_str(args);
} else { } else {
cmd.push_str(&cargo); cmd.push_str(&cargo);
} }
} else { } else {
cmd.push_str(&cargo); cmd.push_str(&cargo);
} }
cmd cmd
} }
pub fn set_ignores(builder: &mut ConfigBuilder, matches: &ArgMatches) { pub fn set_ignores(builder: &mut ConfigBuilder, matches: &ArgMatches) {
if matches.is_present("ignore-nothing") { if matches.is_present("ignore-nothing") {
debug!("Ignoring nothing"); debug!("Ignoring nothing");
builder.no_vcs_ignore(true); builder.no_vcs_ignore(true);
builder.no_ignore(true); builder.no_ignore(true);
return; return;
} }
let novcs = matches.is_present("no-gitignore"); let novcs = matches.is_present("no-gitignore");
builder.no_vcs_ignore(novcs); builder.no_vcs_ignore(novcs);
debug!("Load Git/VCS ignores: {:?}", !novcs); debug!("Load Git/VCS ignores: {:?}", !novcs);
let noignore = matches.is_present("no-ignore"); let noignore = matches.is_present("no-ignore");
builder.no_ignore(noignore); builder.no_ignore(noignore);
debug!("Load .ignore ignores: {:?}", !noignore); debug!("Load .ignore ignores: {:?}", !noignore);
let mut list = vec![ let mut list = vec![
// Mac // Mac
format!("*{}.DS_Store", MAIN_SEPARATOR), format!("*{}.DS_Store", MAIN_SEPARATOR),
// Vim // Vim
"*.sw?".into(), "*.sw?".into(),
"*.sw?x".into(), "*.sw?x".into(),
// Emacs // Emacs
"#*#".into(), "#*#".into(),
".#*".into(), ".#*".into(),
// Kate // Kate
".*.kate-swp".into(), ".*.kate-swp".into(),
// VCS // VCS
format!("*{s}.hg{s}**", s = MAIN_SEPARATOR), format!("*{s}.hg{s}**", s = MAIN_SEPARATOR),
format!("*{s}.git{s}**", s = MAIN_SEPARATOR), format!("*{s}.git{s}**", s = MAIN_SEPARATOR),
format!("*{s}.svn{s}**", s = MAIN_SEPARATOR), format!("*{s}.svn{s}**", s = MAIN_SEPARATOR),
// SQLite // SQLite
"*.db".into(), "*.db".into(),
"*.db-*".into(), "*.db-*".into(),
format!("*{s}*.db-journal{s}**", s = MAIN_SEPARATOR), format!("*{s}*.db-journal{s}**", s = MAIN_SEPARATOR),
// Rust // Rust
format!("*{s}target{s}**", s = MAIN_SEPARATOR), format!("*{s}target{s}**", s = MAIN_SEPARATOR),
]; ];
debug!("Default ignores: {:?}", list); debug!("Default ignores: {:?}", list);
if matches.is_present("ignore") { if matches.is_present("ignore") {
for ignore in values_t!(matches, "ignore", String).unwrap_or_else(|e| e.exit()) { for ignore in values_t!(matches, "ignore", String).unwrap_or_else(|e| e.exit()) {
#[cfg(windows)] #[cfg(windows)]
let ignore = ignore.replace("/", &MAIN_SEPARATOR.to_string()); let ignore = ignore.replace("/", &MAIN_SEPARATOR.to_string());
list.push(ignore); list.push(ignore);
} }
} }
debug!("All ignores: {:?}", list); debug!("All ignores: {:?}", list);
builder.ignores(list); builder.ignores(list);
} }
pub fn set_debounce(builder: &mut ConfigBuilder, matches: &ArgMatches) { pub fn set_debounce(builder: &mut ConfigBuilder, matches: &ArgMatches) {
if matches.is_present("delay") { if matches.is_present("delay") {
let debounce = value_t!(matches, "delay", f32).unwrap_or_else(|e| e.exit()); let debounce = value_t!(matches, "delay", f32).unwrap_or_else(|e| e.exit());
debug!("File updates debounce: {} seconds", debounce); debug!("File updates debounce: {} seconds", debounce);
let d = Duration::from_millis((debounce * 1000.0) as u64); let d = Duration::from_millis((debounce * 1000.0) as u64);
builder.poll_interval(d).debounce(d); builder.poll_interval(d).debounce(d);
} }
} }
pub fn set_watches(builder: &mut ConfigBuilder, matches: &ArgMatches) { pub fn set_watches(builder: &mut ConfigBuilder, matches: &ArgMatches) {
let mut opts = Vec::new(); let mut opts = Vec::new();
if matches.is_present("watch") { if matches.is_present("watch") {
for watch in values_t!(matches, "watch", String).unwrap_or_else(|e| e.exit()) { for watch in values_t!(matches, "watch", String).unwrap_or_else(|e| e.exit()) {
opts.push(watch.into()); opts.push(watch.into());
} }
} }
if opts.is_empty() { if opts.is_empty() {
opts.push(".".into()); opts.push(".".into());
} }
debug!("Watches: {:?}", opts); debug!("Watches: {:?}", opts);
builder.paths(opts); builder.paths(opts);
} }
pub fn get_options(matches: &ArgMatches) -> Config { pub fn get_options(matches: &ArgMatches) -> Config {
let mut builder = ConfigBuilder::default(); let mut builder = ConfigBuilder::default();
builder builder
.poll(matches.is_present("poll")) .poll(matches.is_present("poll"))
.clear_screen(matches.is_present("clear")) .clear_screen(matches.is_present("clear"))
.run_initially(!matches.is_present("postpone")) .run_initially(!matches.is_present("postpone"))
.no_environment(true); .no_environment(true);
// TODO in 8.0: remove --watch-when-idle and switch --no-restart behaviour to DoNothing // TODO in 8.0: remove --watch-when-idle and switch --no-restart behaviour to DoNothing
builder.on_busy_update(if matches.is_present("no-restart") { builder.on_busy_update(if matches.is_present("no-restart") {
OnBusyUpdate::Queue OnBusyUpdate::Queue
} else if matches.is_present("watch-when-idle") { } else if matches.is_present("watch-when-idle") {
OnBusyUpdate::DoNothing OnBusyUpdate::DoNothing
} else { } else {
OnBusyUpdate::Restart OnBusyUpdate::Restart
}); });
builder.shell(if let Some(s) = matches.value_of("use-shell") { builder.shell(if let Some(s) = matches.value_of("use-shell") {
if s.eq_ignore_ascii_case("powershell") { if s.eq_ignore_ascii_case("powershell") {
Shell::Powershell Shell::Powershell
} else if s.eq_ignore_ascii_case("none") { } else if s.eq_ignore_ascii_case("none") {
warn!("--use-shell=none is non-sensical for cargo-watch, ignoring"); warn!("--use-shell=none is non-sensical for cargo-watch, ignoring");
default_shell() default_shell()
} else if s.eq_ignore_ascii_case("cmd") { } else if s.eq_ignore_ascii_case("cmd") {
cmd_shell(s.into()) cmd_shell(s.into())
} else { } else {
Shell::Unix(s.into()) Shell::Unix(s.into())
} }
} else { } else {
// in 8.0, just rely on default watchexec behaviour // in 8.0, just rely on default watchexec behaviour
default_shell() default_shell()
}); });
set_ignores(&mut builder, matches); set_ignores(&mut builder, matches);
set_debounce(&mut builder, matches); set_debounce(&mut builder, matches);
set_watches(&mut builder, matches); set_watches(&mut builder, matches);
set_commands(&mut builder, matches); set_commands(&mut builder, matches);
let mut args = builder.build().unwrap(); let mut args = builder.build().unwrap();
args.once = matches.is_present("once"); args.once = matches.is_present("once");
debug!("Watchexec arguments: {:?}", args); debug!("Watchexec arguments: {:?}", args);
args args
} }
// until 8.0 // until 8.0
#[cfg(windows)] #[cfg(windows)]
fn default_shell() -> Shell { fn default_shell() -> Shell {
Shell::Cmd Shell::Cmd
} }
#[cfg(not(windows))] #[cfg(not(windows))]
fn default_shell() -> Shell { fn default_shell() -> Shell {
Shell::default() Shell::default()
} }
// because Shell::Cmd is only on windows // because Shell::Cmd is only on windows
#[cfg(windows)] #[cfg(windows)]
fn cmd_shell(_: String) -> Shell { fn cmd_shell(_: String) -> Shell {
Shell::Cmd Shell::Cmd
} }
#[cfg(not(windows))] #[cfg(not(windows))]
fn cmd_shell(s: String) -> Shell { fn cmd_shell(s: String) -> Shell {
Shell::Unix(s) Shell::Unix(s)
} }

View File

@ -4,24 +4,24 @@ use log::debug;
use std::{env::set_current_dir, process::Command}; use std::{env::set_current_dir, process::Command};
pub fn project_root() -> Utf8PathBuf { pub fn project_root() -> Utf8PathBuf {
Command::new("cargo") Command::new("cargo")
.arg("locate-project") .arg("locate-project")
.arg("--message-format") .arg("--message-format")
.arg("plain") .arg("plain")
.output() .output()
.map_err(|err| err.to_string()) .map_err(|err| err.to_string())
.and_then(|out| String::from_utf8(out.stdout).map_err(|err| err.to_string())) .and_then(|out| String::from_utf8(out.stdout).map_err(|err| err.to_string()))
.map(Utf8PathBuf::from) .map(Utf8PathBuf::from)
.and_then(|path| { .and_then(|path| {
path.parent() path.parent()
.ok_or_else(|| String::from("project root does not exist")) .ok_or_else(|| String::from("project root does not exist"))
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
}) })
.unwrap_or_else(|err| Error::with_description(&err, ErrorKind::Io).exit()) .unwrap_or_else(|err| Error::with_description(&err, ErrorKind::Io).exit())
} }
pub fn change_dir(dir: Utf8PathBuf) { pub fn change_dir(dir: Utf8PathBuf) {
debug!("change directory to: {}", dir); debug!("change directory to: {}", dir);
set_current_dir(dir) set_current_dir(dir)
.unwrap_or_else(|err| Error::with_description(&err.to_string(), ErrorKind::Io).exit()) .unwrap_or_else(|err| Error::with_description(&err.to_string(), ErrorKind::Io).exit())
} }

View File

@ -1,80 +1,80 @@
use watchexec::{ use watchexec::{
config::Config, config::Config,
error::Result, error::Result,
pathop::PathOp, pathop::PathOp,
run::{ExecHandler, Handler}, run::{ExecHandler, Handler},
}; };
pub struct CwHandler { pub struct CwHandler {
cmd: String, cmd: String,
once: bool, once: bool,
quiet: bool, quiet: bool,
notify: bool, notify: bool,
inner: ExecHandler, inner: ExecHandler,
} }
impl Handler for CwHandler { impl Handler for CwHandler {
fn args(&self) -> Config { fn args(&self) -> Config {
self.inner.args() self.inner.args()
} }
fn on_manual(&self) -> Result<bool> { fn on_manual(&self) -> Result<bool> {
if self.once { if self.once {
Ok(true) Ok(true)
} else { } else {
self.start(); self.start();
self.inner.on_manual() self.inner.on_manual()
} }
} }
fn on_update(&self, ops: &[PathOp]) -> Result<bool> { fn on_update(&self, ops: &[PathOp]) -> Result<bool> {
self.start(); self.start();
self.inner.on_update(ops).map(|o| { self.inner.on_update(ops).map(|o| {
#[cfg(not(target_os = "freebsd"))] #[cfg(not(target_os = "freebsd"))]
if self.notify { if self.notify {
notify_rust::Notification::new() notify_rust::Notification::new()
.summary("Cargo Watch observed a change") .summary("Cargo Watch observed a change")
.body("Cargo Watch has seen a change, the command may have restarted.") .body("Cargo Watch has seen a change, the command may have restarted.")
.show() .show()
.map(drop) .map(drop)
.unwrap_or_else(|err| { .unwrap_or_else(|err| {
log::warn!("Failed to send desktop notification: {}", err); log::warn!("Failed to send desktop notification: {}", err);
}); });
} }
o o
}) })
} }
} }
impl CwHandler { impl CwHandler {
pub fn new(mut args: Config, quiet: bool, notify: bool) -> Result<Self> { pub fn new(mut args: Config, quiet: bool, notify: bool) -> Result<Self> {
let cmd = args.cmd.join(" && "); let cmd = args.cmd.join(" && ");
let mut final_cmd = cmd.clone(); let mut final_cmd = cmd.clone();
if !quiet { if !quiet {
#[cfg(unix)] #[cfg(unix)]
final_cmd.push_str(r#"; echo "[Finished running. Exit status: $?]""#); final_cmd.push_str(r#"; echo "[Finished running. Exit status: $?]""#);
#[cfg(windows)] #[cfg(windows)]
final_cmd.push_str(r#" & echo "[Finished running. Exit status: %ERRORLEVEL%]""#); final_cmd.push_str(r#" & echo "[Finished running. Exit status: %ERRORLEVEL%]""#);
#[cfg(not(any(unix, windows)))] #[cfg(not(any(unix, windows)))]
final_cmd.push_str(r#" ; echo "[Finished running]""#); final_cmd.push_str(r#" ; echo "[Finished running]""#);
// ^ could be wrong depending on the platform, to be fixed on demand // ^ could be wrong depending on the platform, to be fixed on demand
} }
args.cmd = vec![final_cmd]; args.cmd = vec![final_cmd];
Ok(Self { Ok(Self {
once: args.once, once: args.once,
cmd, cmd,
inner: ExecHandler::new(args)?, inner: ExecHandler::new(args)?,
quiet, quiet,
notify, notify,
}) })
} }
fn start(&self) { fn start(&self) {
if !self.quiet { if !self.quiet {
println!("[Running '{}']", self.cmd); println!("[Running '{}']", self.cmd);
} }
} }
} }

View File

@ -1,197 +1,197 @@
use assert_cmd::prelude::*; use assert_cmd::prelude::*;
use std::{ use std::{
fs::OpenOptions, fs::OpenOptions,
io::{self, Write}, io::{self, Write},
path::PathBuf, path::PathBuf,
process::{Command, Stdio}, process::{Command, Stdio},
thread::sleep, thread::sleep,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use wait_timeout::ChildExt; use wait_timeout::ChildExt;
fn touch(n: u8) -> io::Result<()> { fn touch(n: u8) -> io::Result<()> {
let path: PathBuf = format!("./tests/touchdata/{}.txt", n).into(); let path: PathBuf = format!("./tests/touchdata/{}.txt", n).into();
let mut file = OpenOptions::new().create(true).write(true).open(path)?; let mut file = OpenOptions::new().create(true).write(true).open(path)?;
writeln!(&mut file, "{:?}", Instant::now())?; writeln!(&mut file, "{:?}", Instant::now())?;
Ok(()) Ok(())
} }
fn std_to_string<T: io::Read>(handle: &mut Option<T>) -> String { fn std_to_string<T: io::Read>(handle: &mut Option<T>) -> String {
if let Some(ref mut handle) = handle { if let Some(ref mut handle) = handle {
let mut buf = String::with_capacity(1024); let mut buf = String::with_capacity(1024);
handle.read_to_string(&mut buf).unwrap(); handle.read_to_string(&mut buf).unwrap();
buf buf
} else { } else {
unreachable!() unreachable!()
} }
} }
// fsevents has trouble // fsevents has trouble
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
#[test] #[test]
fn without_poll() { fn without_poll() {
let mut main = Command::cargo_bin("cargo-watch") let mut main = Command::cargo_bin("cargo-watch")
.unwrap() .unwrap()
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.args(&[ .args(&[
"--testing-only--once", "--testing-only--once",
"--no-gitignore", "--no-gitignore",
"-w", "-w",
"./tests/touchdata/", "./tests/touchdata/",
"-s", "-s",
"echo it runs", "echo it runs",
]) ])
.spawn() .spawn()
.unwrap(); .unwrap();
sleep(Duration::from_secs(2)); sleep(Duration::from_secs(2));
touch(0).unwrap(); touch(0).unwrap();
if main if main
.wait_timeout(Duration::from_secs(30)) .wait_timeout(Duration::from_secs(30))
.unwrap() .unwrap()
.is_none() .is_none()
{ {
main.kill().unwrap(); main.kill().unwrap();
} }
main.wait_with_output().unwrap().assert().success(); main.wait_with_output().unwrap().assert().success();
} }
#[test] #[test]
fn with_poll() { fn with_poll() {
let mut main = Command::cargo_bin("cargo-watch") let mut main = Command::cargo_bin("cargo-watch")
.unwrap() .unwrap()
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.args(&[ .args(&[
"--testing-only--once", "--testing-only--once",
"--no-gitignore", "--no-gitignore",
"--poll", "--poll",
"-w", "-w",
"./tests/touchdata/", "./tests/touchdata/",
"-s", "-s",
"echo it runs", "echo it runs",
]) ])
.spawn() .spawn()
.unwrap(); .unwrap();
sleep(Duration::from_secs(2)); sleep(Duration::from_secs(2));
touch(1).unwrap(); touch(1).unwrap();
if main if main
.wait_timeout(Duration::from_secs(30)) .wait_timeout(Duration::from_secs(30))
.unwrap() .unwrap()
.is_none() .is_none()
{ {
main.kill().unwrap(); main.kill().unwrap();
} }
main.wait_with_output().unwrap().assert().success(); main.wait_with_output().unwrap().assert().success();
} }
#[test] #[test]
#[cfg(not(windows))] // annoyingly, theres some kind of encoding or extra bytes getting added here, needs debugging #[cfg(not(windows))] // annoyingly, theres some kind of encoding or extra bytes getting added here, needs debugging
fn with_announce() { fn with_announce() {
let mut main = Command::cargo_bin("cargo-watch") let mut main = Command::cargo_bin("cargo-watch")
.unwrap() .unwrap()
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.args(&[ .args(&[
"--testing-only--once", "--testing-only--once",
"--no-gitignore", "--no-gitignore",
"--poll", "--poll",
"-w", "-w",
"./tests/touchdata/", "./tests/touchdata/",
"-s", "-s",
"echo with announce", "echo with announce",
]) ])
.spawn() .spawn()
.unwrap(); .unwrap();
sleep(Duration::from_secs(2)); sleep(Duration::from_secs(2));
touch(2).unwrap(); touch(2).unwrap();
if main if main
.wait_timeout(Duration::from_secs(30)) .wait_timeout(Duration::from_secs(30))
.unwrap() .unwrap()
.is_none() .is_none()
{ {
main.kill().unwrap(); main.kill().unwrap();
} }
insta::assert_snapshot!("with_announce.stderr", std_to_string(&mut main.stderr)); insta::assert_snapshot!("with_announce.stderr", std_to_string(&mut main.stderr));
insta::assert_snapshot!("with_announce.stdout", std_to_string(&mut main.stdout)); insta::assert_snapshot!("with_announce.stdout", std_to_string(&mut main.stdout));
} }
#[test] #[test]
fn without_announce() { fn without_announce() {
let mut main = Command::cargo_bin("cargo-watch") let mut main = Command::cargo_bin("cargo-watch")
.unwrap() .unwrap()
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.args(&[ .args(&[
"--testing-only--once", "--testing-only--once",
"--no-gitignore", "--no-gitignore",
"--quiet", "--quiet",
"--poll", "--poll",
"-w", "-w",
"./tests/touchdata/", "./tests/touchdata/",
"-s", "-s",
"echo without announce", "echo without announce",
]) ])
.spawn() .spawn()
.unwrap(); .unwrap();
sleep(Duration::from_secs(2)); sleep(Duration::from_secs(2));
touch(3).unwrap(); touch(3).unwrap();
if main if main
.wait_timeout(Duration::from_secs(30)) .wait_timeout(Duration::from_secs(30))
.unwrap() .unwrap()
.is_none() .is_none()
{ {
main.kill().unwrap(); main.kill().unwrap();
} }
insta::assert_snapshot!("without_announce.stderr", std_to_string(&mut main.stderr)); insta::assert_snapshot!("without_announce.stderr", std_to_string(&mut main.stderr));
insta::assert_snapshot!("without_announce.stdout", std_to_string(&mut main.stdout)); insta::assert_snapshot!("without_announce.stdout", std_to_string(&mut main.stdout));
} }
#[cfg(unix)] #[cfg(unix)]
#[test] #[test]
fn with_error() { fn with_error() {
let mut main = Command::cargo_bin("cargo-watch") let mut main = Command::cargo_bin("cargo-watch")
.unwrap() .unwrap()
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.args(&[ .args(&[
"--testing-only--once", "--testing-only--once",
"--no-gitignore", "--no-gitignore",
"--poll", "--poll",
"-w", "-w",
"./tests/touchdata/", "./tests/touchdata/",
"-s", "-s",
"echo with error", "echo with error",
"-s", "-s",
"false", "false",
]) ])
.spawn() .spawn()
.unwrap(); .unwrap();
sleep(Duration::from_secs(2)); sleep(Duration::from_secs(2));
touch(4).unwrap(); touch(4).unwrap();
if main if main
.wait_timeout(Duration::from_secs(30)) .wait_timeout(Duration::from_secs(30))
.unwrap() .unwrap()
.is_none() .is_none()
{ {
main.kill().unwrap(); main.kill().unwrap();
} }
insta::assert_snapshot!("with_error.stderr", std_to_string(&mut main.stderr)); insta::assert_snapshot!("with_error.stderr", std_to_string(&mut main.stderr));
insta::assert_snapshot!("with_error.stdout", std_to_string(&mut main.stdout)); insta::assert_snapshot!("with_error.stdout", std_to_string(&mut main.stdout));
} }

View File

@ -1,69 +1,69 @@
use assert_cmd::prelude::*; use assert_cmd::prelude::*;
use predicates::str::is_match; use predicates::str::is_match;
use std::{ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
time::Duration, time::Duration,
}; };
use wait_timeout::ChildExt; use wait_timeout::ChildExt;
#[test] #[test]
fn with_cargo() { fn with_cargo() {
let mut main = Command::new("cargo") let mut main = Command::new("cargo")
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.args(&["watch", "--version"]) .args(&["watch", "--version"])
.spawn() .spawn()
.unwrap(); .unwrap();
if main.wait_timeout(Duration::from_secs(1)).unwrap().is_none() { if main.wait_timeout(Duration::from_secs(1)).unwrap().is_none() {
main.kill().unwrap(); main.kill().unwrap();
} }
main.wait_with_output() main.wait_with_output()
.unwrap() .unwrap()
.assert() .assert()
.success() .success()
.stdout(is_match(r"cargo-watch \d+\.\d+\.\d+\n").unwrap()); .stdout(is_match(r"cargo-watch \d+\.\d+\.\d+\n").unwrap());
} }
#[test] #[test]
fn without_cargo() { fn without_cargo() {
let mut main = Command::cargo_bin("cargo-watch") let mut main = Command::cargo_bin("cargo-watch")
.unwrap() .unwrap()
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.args(&["watch", "--version"]) .args(&["watch", "--version"])
.spawn() .spawn()
.unwrap(); .unwrap();
if main.wait_timeout(Duration::from_secs(1)).unwrap().is_none() { if main.wait_timeout(Duration::from_secs(1)).unwrap().is_none() {
main.kill().unwrap(); main.kill().unwrap();
} }
main.wait_with_output() main.wait_with_output()
.unwrap() .unwrap()
.assert() .assert()
.success() .success()
.stdout(is_match(r"cargo-watch \d+\.\d+\.\d+\n").unwrap()); .stdout(is_match(r"cargo-watch \d+\.\d+\.\d+\n").unwrap());
} }
#[test] #[test]
fn without_watch() { fn without_watch() {
let mut main = Command::cargo_bin("cargo-watch") let mut main = Command::cargo_bin("cargo-watch")
.unwrap() .unwrap()
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.args(&["--version"]) .args(&["--version"])
.spawn() .spawn()
.unwrap(); .unwrap();
if main.wait_timeout(Duration::from_secs(1)).unwrap().is_none() { if main.wait_timeout(Duration::from_secs(1)).unwrap().is_none() {
main.kill().unwrap(); main.kill().unwrap();
} }
main.wait_with_output() main.wait_with_output()
.unwrap() .unwrap()
.assert() .assert()
.success() .success()
.stdout(is_match(r"cargo-watch \d+\.\d+\.\d+\n").unwrap()); .stdout(is_match(r"cargo-watch \d+\.\d+\.\d+\n").unwrap());
} }