Skip to content

Commit ad15990

Browse files
committed
feat: add support for watcher to tests
1 parent 4eeaee4 commit ad15990

13 files changed

Lines changed: 394 additions & 59 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ hone/
9292
- Prefer `&str` over `String` for function parameters when ownership isn't needed
9393
- Use `Option` and `Result` idiomatically; avoid sentinel values
9494
- Prefer explicit error types over `unwrap` / `expect` patterns
95+
- Use `.to_string_lossy().into_owned()` instead of `.to_string_lossy().to_string()` for converting to owned string values
9596

9697
### Source Control Conventions
9798

Cargo.lock

Lines changed: 127 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ dirs = "5"
3131
toml_edit = "0.22"
3232
relnotify = "1"
3333
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls", "native-tls-vendored"] }
34+
notify = "8"
35+
notify-debouncer-full = "0.6"
3436

3537
[dev-dependencies]
3638
assert_matches = "1"

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ pub mod assertions;
22
pub mod lsp;
33
pub mod parser;
44
pub mod runner;
5+
pub mod watcher;
56

67
pub use lsp::run_lsp_server;
78
pub use parser::{parse_file, HoneFile, ParseResult};
89
pub use runner::{run_tests, OutputFormat, RunnerOptions, TestRunOutput};
10+
pub use watcher::run_watch_mode;

src/main.rs

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use clap::{Parser, Subcommand};
2-
use hone::{run_lsp_server, run_tests, OutputFormat, RunnerOptions};
2+
use hone::{run_lsp_server, run_tests, run_watch_mode, OutputFormat, RunnerOptions};
33

44
mod setup;
55
mod update;
@@ -31,6 +31,10 @@ struct Cli {
3131
/// Output format
3232
#[arg(long = "output-format", value_enum, default_value = "text")]
3333
output_format: OutputFormat,
34+
35+
/// Watch mode: re-run tests when files change
36+
#[arg(long, short)]
37+
watch: bool,
3438
}
3539

3640
#[derive(Subcommand)]
@@ -55,6 +59,10 @@ enum Commands {
5559
/// Output format
5660
#[arg(long = "output-format", value_enum, default_value = "text")]
5761
output_format: OutputFormat,
62+
63+
/// Watch mode: re-run tests when files change
64+
#[arg(long, short)]
65+
watch: bool,
5866
},
5967
/// Start the Language Server Protocol (LSP) server
6068
Lsp,
@@ -125,16 +133,22 @@ async fn main() -> anyhow::Result<()> {
125133
verbose,
126134
test_filter,
127135
output_format,
136+
watch,
128137
}) => {
129138
let options = RunnerOptions {
130139
shell,
131140
verbose,
132141
test_filter,
133142
output_format,
134143
};
135-
let results = run_tests(patterns, options).await?;
136-
update::show_update_notification_if_available();
137-
std::process::exit(if results.has_failures() { 1 } else { 0 });
144+
if watch {
145+
run_watch_mode(patterns, options).await?;
146+
Ok(())
147+
} else {
148+
let results = run_tests(patterns, options).await?;
149+
update::show_update_notification_if_available();
150+
std::process::exit(if results.has_failures() { 1 } else { 0 });
151+
}
138152
}
139153
None => {
140154
let options = RunnerOptions {
@@ -143,9 +157,14 @@ async fn main() -> anyhow::Result<()> {
143157
test_filter: cli.test_filter,
144158
output_format: cli.output_format,
145159
};
146-
let results = run_tests(cli.patterns, options).await?;
147-
update::show_update_notification_if_available();
148-
std::process::exit(if results.has_failures() { 1 } else { 0 });
160+
if cli.watch {
161+
run_watch_mode(cli.patterns, options).await?;
162+
Ok(())
163+
} else {
164+
let results = run_tests(cli.patterns, options).await?;
165+
update::show_update_notification_if_available();
166+
std::process::exit(if results.has_failures() { 1 } else { 0 });
167+
}
149168
}
150169
}
151170
}

src/runner/executor.rs

Lines changed: 3 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ use crate::runner::reporter::{
1414
};
1515
use crate::runner::shell::{create_shell_config, RunResult, ShellSession};
1616
use regex::Regex;
17-
use std::collections::{BTreeSet, HashMap};
18-
use std::path::{Path, PathBuf};
17+
use std::collections::HashMap;
18+
use std::path::Path;
1919
use std::time::{SystemTime, UNIX_EPOCH};
2020

2121
#[derive(Debug, Clone, Default)]
@@ -79,7 +79,6 @@ pub async fn run_tests(
7979
) -> anyhow::Result<TestRunOutput> {
8080
let is_json = options.output_format == OutputFormat::Json;
8181
let reporter = DefaultReporter::new(options.verbose, options.output_format);
82-
let cwd = std::env::current_dir()?.to_string_lossy().to_string();
8382
let start_time = std::time::Instant::now();
8483
let start_epoch = SystemTime::now()
8584
.duration_since(UNIX_EPOCH)
@@ -98,12 +97,7 @@ pub async fn run_tests(
9897
None
9998
};
10099

101-
let mut all_files = BTreeSet::new();
102-
for pattern in &patterns {
103-
let files = resolve_files(pattern, &cwd).await?;
104-
all_files.extend(files);
105-
}
106-
let all_files: Vec<_> = all_files.into_iter().collect();
100+
let all_files = crate::runner::resolve_patterns(&patterns).await?;
107101

108102
if all_files.is_empty() {
109103
if !is_json {
@@ -265,47 +259,6 @@ pub async fn run_tests(
265259
Ok(output)
266260
}
267261

268-
async fn resolve_files(pattern: &str, cwd: &str) -> anyhow::Result<Vec<String>> {
269-
let resolved = PathBuf::from(cwd).join(pattern);
270-
271-
// Check if it's a direct file
272-
if let Ok(metadata) = tokio::fs::metadata(&resolved).await {
273-
if metadata.is_file() && pattern.ends_with(".hone") {
274-
return Ok(vec![resolved.to_string_lossy().to_string()]);
275-
}
276-
277-
if metadata.is_dir() {
278-
// Use glob to find all .hone files in the directory
279-
let pattern = format!("{}/**/*.hone", resolved.to_string_lossy());
280-
let paths = glob::glob(&pattern)?;
281-
let results: Vec<String> = paths
282-
.filter_map(Result::ok)
283-
.filter(|p| p.is_file())
284-
.map(|p| p.to_string_lossy().to_string())
285-
.collect();
286-
return Ok(results);
287-
}
288-
}
289-
290-
// Use glob for pattern matching
291-
let glob_pattern = if Path::new(pattern).is_absolute() {
292-
pattern.to_string()
293-
} else {
294-
PathBuf::from(cwd)
295-
.join(pattern)
296-
.to_string_lossy()
297-
.to_string()
298-
};
299-
300-
let paths = glob::glob(&glob_pattern)?;
301-
let results: Vec<String> = paths
302-
.filter_map(Result::ok)
303-
.filter(|p| p.is_file())
304-
.map(|p| p.to_string_lossy().to_string())
305-
.collect();
306-
Ok(results)
307-
}
308-
309262
async fn run_file(
310263
ast: &[ASTNode],
311264
filename: &str,

0 commit comments

Comments
 (0)