1use std::{
8 env::current_dir,
9 io::{self, Write},
10 path::Path,
11 sync::mpsc,
12 time::Duration,
13};
14
15use notify::{RecommendedWatcher, RecursiveMode, Watcher};
16
17use crate::{Config, TestFailure, error::LSError, runner::DynRunner, workspace};
18
19pub async fn run(config: Config) -> Result<(), LSError> {
21 let project_dir = config
22 .workspace_root
23 .clone()
24 .or_else(|| current_dir().ok())
25 .ok_or(LSError::NoWorkspaceFolders)?;
26
27 eprintln!("backtrace-ls: watching {}", project_dir.display());
28
29 let runner_name = config
31 .runner
32 .clone()
33 .map_or_else(|| workspace::detect_runner(&project_dir), Ok)?;
34
35 let runner = <dyn DynRunner>::from_name(&runner_name)?;
36 eprintln!("backtrace-ls: using runner '{runner_name}'");
37
38 run_tests(&project_dir, runner, &config.extra_args).await?;
40
41 let (tx, rx) = mpsc::channel();
43 let mut watcher: RecommendedWatcher = Watcher::new(
44 tx,
45 notify::Config::default().with_poll_interval(Duration::from_secs(1)),
46 )
47 .map_err(|e| LSError::Generic(format!("Failed to create watcher: {e}")))?;
48
49 watcher
50 .watch(&project_dir, RecursiveMode::Recursive)
51 .map_err(|e| LSError::Generic(format!("Failed to watch directory: {e}")))?;
52
53 eprintln!("backtrace-ls: watching for changes...\n");
54
55 loop {
57 match rx.recv() {
58 Ok(Ok(event)) => {
59 if should_rerun(&event, runner) {
60 eprintln!("\n--- File changed, re-running tests ---\n");
61 if let Err(e) = run_tests(&project_dir, runner, &config.extra_args).await {
62 eprintln!("backtrace-ls: error running tests: {e}");
63 }
64 }
65 }
66 Ok(Err(e)) => {
67 eprintln!("backtrace-ls: watch error: {e}");
68 }
69 Err(e) => {
70 eprintln!("backtrace-ls: channel error: {e}");
71 break;
72 }
73 }
74 }
75
76 Ok(())
77}
78
79fn should_rerun(event: ¬ify::Event, runner: &'static dyn DynRunner) -> bool {
80 use notify::EventKind;
81
82 if !matches!(
84 event.kind,
85 EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
86 ) {
87 return false;
88 }
89
90 event.paths.iter().any(|path| {
92 path.extension()
93 .and_then(|e| e.to_str())
94 .is_some_and(|ext| runner.file_extensions().contains(&ext))
95 })
96}
97
98async fn run_tests(
99 project_dir: &Path,
100 runner: &'static dyn DynRunner,
101 extra_args: &[String],
102) -> Result<(), LSError> {
103 let file_paths = workspace::walk_files(project_dir, runner.file_extensions());
104
105 if file_paths.is_empty() {
106 eprintln!("No test files found");
107 return Ok(());
108 }
109
110 let workspaces = runner.detect_workspaces(&file_paths);
111 let mut total_failures = 0;
112
113 for (workspace, paths) in workspaces.map {
114 eprintln!(
115 "Running {} on {} files in {}",
116 runner.name(),
117 paths.len(),
118 workspace.display()
119 );
120
121 match runner.run(&paths, &workspace, extra_args).await {
122 Ok(failures) => {
123 total_failures += failures.len();
124 print_failures(&failures);
125 }
126 Err(e) => {
127 eprintln!(" Error: {e}");
128 }
129 }
130 }
131
132 if total_failures == 0 {
133 println!("\nAll tests passed!");
134 } else {
135 println!("\n{total_failures} test failure(s)");
136 }
137
138 Ok(())
139}
140
141fn print_failures(failures: &[TestFailure]) {
142 let stdout = io::stdout();
143 let mut handle = stdout.lock();
144
145 for failure in failures {
146 writeln!(handle).ok();
147
148 if let Some(ctx) = &failure.context {
150 writeln!(
151 handle,
152 "FAIL: {} ({}:{})",
153 ctx.name,
154 ctx.span.path.display(),
155 ctx.span.range.start.line + 1
156 )
157 .ok();
158 } else {
159 writeln!(handle, "FAIL: {}", failure.failure_id()).ok();
160 }
161
162 if let Some(user_failure) = &failure.user_facing_failure {
164 writeln!(
165 handle,
166 " at {}:{}",
167 user_failure.span.path.display(),
168 user_failure.span.range.start.line + 1
169 )
170 .ok();
171
172 for line in user_failure.message.lines() {
174 writeln!(handle, " | {line}").ok();
175 }
176 }
177
178 if !failure.stack_frames.is_empty() {
180 writeln!(handle, " backtrace:").ok();
181 for (i, frame) in failure.stack_frames.iter().enumerate() {
182 writeln!(
183 handle,
184 " {i}: {} ({}:{})",
185 frame.function_name,
186 frame.path.display(),
187 frame.range.start.line + 1
188 )
189 .ok();
190 }
191 }
192 }
193}