22
33use anyhow:: Result ;
44use clap:: Args ;
5+ use forge_runtime:: isolate:: v8_runtime:: ForgeRuntime ;
6+ use notify:: { Config , RecommendedWatcher , RecursiveMode , Watcher } ;
7+ use std:: path:: { Path , PathBuf } ;
8+ use std:: sync:: Arc ;
9+ use tokio:: sync:: Notify ;
10+ use tokio:: time:: { sleep, Duration } ;
511
612#[ derive( Debug , Args ) ]
713pub struct TestArgs {
@@ -12,7 +18,144 @@ pub struct TestArgs {
1218 pub filter : Option < String > ,
1319}
1420
15- pub async fn run ( _args : TestArgs ) -> Result < ( ) > {
16- // TODO: Implement test runner
21+ fn find_tests < ' a > (
22+ dir : & ' a Path ,
23+ filter : Option < & ' a str > ,
24+ ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < Vec < PathBuf > > > + Send + ' a > > {
25+ Box :: pin ( async move {
26+ let mut tests = Vec :: new ( ) ;
27+ let mut entries = tokio:: fs:: read_dir ( dir) . await ?;
28+
29+ while let Ok ( Some ( entry) ) = entries. next_entry ( ) . await {
30+ let path = entry. path ( ) ;
31+ if path. is_dir ( ) {
32+ let name = path. file_name ( ) . unwrap_or_default ( ) . to_string_lossy ( ) ;
33+ if name != "node_modules" && name != "dist" && name != "target" {
34+ tests. extend ( find_tests ( & path, filter) . await ?) ;
35+ }
36+ } else if let Some ( ext) = path. extension ( ) {
37+ if ext == "ts" || ext == "fx" {
38+ let name = path. file_name ( ) . unwrap_or_default ( ) . to_string_lossy ( ) ;
39+ if name. ends_with ( ".test.ts" ) || name. ends_with ( ".test.fx" ) {
40+ if let Some ( f) = filter {
41+ if !name. contains ( f) && !path. to_string_lossy ( ) . contains ( f) {
42+ continue ;
43+ }
44+ }
45+ tests. push ( path) ;
46+ }
47+ }
48+ }
49+ }
50+ Ok ( tests)
51+ } )
52+ }
53+
54+ async fn run_tests ( filter : Option < & str > ) -> Result < ( ) > {
55+ crate :: output:: info ( "Discovering tests..." ) ;
56+ let mut tests = find_tests ( Path :: new ( "." ) , filter) . await ?;
57+ tests. sort ( ) ;
58+
59+ if tests. is_empty ( ) {
60+ crate :: output:: warn ( "No tests found matching the criteria." ) ;
61+ return Ok ( ( ) ) ;
62+ }
63+
64+ crate :: output:: info ( & format ! ( "Found {} test file(s)" , tests. len( ) ) ) ;
65+ let mut passed = 0 ;
66+ let mut failed = 0 ;
67+
68+ let mut runtime = ForgeRuntime :: new ( ) ?;
69+
70+ for test in & tests {
71+ crate :: output:: info ( & format ! ( "Running {}" , test. display( ) ) ) ;
72+
73+ let content = tokio:: fs:: read ( test) . await ?;
74+ let execution_result = runtime. execute_module ( & content) . await ;
75+
76+ match execution_result {
77+ Ok ( _) => {
78+ crate :: output:: success ( & format ! ( "PASS {}" , test. display( ) ) ) ;
79+ passed += 1 ;
80+ }
81+ Err ( e) => {
82+ crate :: output:: error ( & format ! ( "FAIL {}: {}" , test. display( ) , e) ) ;
83+ failed += 1 ;
84+ }
85+ }
86+ }
87+
88+ if failed == 0 {
89+ crate :: output:: success ( & format ! ( "\n Test Summary: {} passed, 0 failed" , passed) ) ;
90+ } else {
91+ crate :: output:: error ( & format ! (
92+ "\n Test Summary: {} passed, {} failed" ,
93+ passed, failed
94+ ) ) ;
95+ anyhow:: bail!( "{} test(s) failed" , failed) ;
96+ }
97+
1798 Ok ( ( ) )
1899}
100+
101+ fn is_relevant_event ( event : & notify:: Event ) -> bool {
102+ const EXCLUDED : & [ & str ] = & [ "node_modules" , "dist" , "target" ] ;
103+ if event. kind . is_modify ( ) || event. kind . is_create ( ) || event. kind . is_remove ( ) {
104+ for path in & event. paths {
105+ if path. components ( ) . any ( |c| {
106+ let s = c. as_os_str ( ) . to_string_lossy ( ) ;
107+ EXCLUDED . contains ( & s. as_ref ( ) )
108+ } ) {
109+ continue ;
110+ }
111+ if let Some ( ext) = path. extension ( ) {
112+ if ext == "ts" || ext == "fx" {
113+ return true ;
114+ }
115+ }
116+ }
117+ }
118+ false
119+ }
120+
121+ pub async fn run ( args : TestArgs ) -> Result < ( ) > {
122+ let filter = args. filter . as_deref ( ) ;
123+ run_tests ( filter) . await ?;
124+
125+ if args. watch {
126+ watch_loop ( filter) . await ?;
127+ }
128+
129+ Ok ( ( ) )
130+ }
131+
132+ async fn watch_loop ( filter : Option < & str > ) -> Result < ( ) > {
133+ crate :: output:: info ( "Watching for file changes..." ) ;
134+ let notify = Arc :: new ( Notify :: new ( ) ) ;
135+ let notify_clone = Arc :: clone ( & notify) ;
136+
137+ let mut watcher = RecommendedWatcher :: new (
138+ move |res : Result < notify:: Event , notify:: Error > | match res {
139+ Ok ( event) => {
140+ if is_relevant_event ( & event) {
141+ notify_clone. notify_one ( ) ;
142+ }
143+ }
144+ Err ( err) => {
145+ eprintln ! ( "watch error: {err}" ) ;
146+ }
147+ } ,
148+ Config :: default ( ) ,
149+ ) ?;
150+
151+ watcher. watch ( Path :: new ( "." ) , RecursiveMode :: Recursive ) ?;
152+
153+ loop {
154+ notify. notified ( ) . await ;
155+ sleep ( Duration :: from_millis ( 100 ) ) . await ;
156+ crate :: output:: info ( "\n File change detected. Re-running tests..." ) ;
157+ if let Err ( err) = run_tests ( filter) . await {
158+ eprintln ! ( "Error re-running tests: {err}" ) ;
159+ }
160+ }
161+ }
0 commit comments