@@ -4,6 +4,7 @@ use std::fs;
44use std:: path:: { Path , PathBuf } ;
55use std:: process:: { Command , Output } ;
66use std:: sync:: { Mutex , OnceLock } ;
7+ use std:: time:: Duration ;
78
89static BINARY_BUILT : OnceLock < ( ) > = OnceLock :: new ( ) ;
910static PROJECT_LOCK : OnceLock < Mutex < ( ) > > = OnceLock :: new ( ) ;
@@ -38,18 +39,21 @@ pub struct LocalTestRunner {
3839 binary : PathBuf ,
3940 cwd : PathBuf ,
4041 _lock_guard : std:: sync:: MutexGuard < ' static , ( ) > ,
42+ _file_lock : CrossProcessLock ,
4143}
4244
4345impl LocalTestRunner {
4446 /// 默认在 ~/workspace/memory-sync/ 下运行测试。
4547 /// 若该目录不存在,则回退到当前目录。
4648 pub fn new ( ) -> Self {
4749 ensure_binary ( ) ;
48- // 所有测试共享同一个真实项目目录,必须串行执行
50+ // Cross-process lock: serialises test binaries sharing the same project
51+ let file_lock = acquire_cross_process_lock ( ) ;
52+ // In-process lock: serialises tests within a single binary
4953 let guard = PROJECT_LOCK
5054 . get_or_init ( || Mutex :: new ( ( ) ) )
5155 . lock ( )
52- . expect ( "project lock should not be poisoned" ) ;
56+ . unwrap_or_else ( |e| e . into_inner ( ) ) ;
5357 let default_project = home_dir ( ) . join ( "workspace" ) . join ( "memory-sync" ) ;
5458 let cwd = if default_project. is_dir ( ) {
5559 default_project
@@ -60,15 +64,17 @@ impl LocalTestRunner {
6064 binary : binary_path ( ) ,
6165 cwd,
6266 _lock_guard : guard,
67+ _file_lock : file_lock,
6368 }
6469 }
6570
6671 pub fn with_cwd ( cwd : impl AsRef < Path > ) -> Self {
6772 ensure_binary ( ) ;
73+ let file_lock = acquire_cross_process_lock ( ) ;
6874 let guard = PROJECT_LOCK
6975 . get_or_init ( || Mutex :: new ( ( ) ) )
7076 . lock ( )
71- . expect ( "project lock should not be poisoned" ) ;
77+ . unwrap_or_else ( |e| e . into_inner ( ) ) ;
7278 let cwd = cwd. as_ref ( ) . to_path_buf ( ) ;
7379 assert ! (
7480 cwd. is_dir( ) ,
@@ -79,6 +85,7 @@ impl LocalTestRunner {
7985 binary : binary_path ( ) ,
8086 cwd,
8187 _lock_guard : guard,
88+ _file_lock : file_lock,
8289 }
8390 }
8491
@@ -126,6 +133,21 @@ impl LocalTestRunner {
126133 command_output ( & mut cmd, & format ! ( "tnmsc {}" , args. join( " " ) ) )
127134 }
128135
136+ /// 在指定目录下运行 tnmsc 命令,并设置额外环境变量。
137+ pub fn run_at_with_env (
138+ & self ,
139+ cwd : impl AsRef < Path > ,
140+ args : & [ & str ] ,
141+ envs : & [ ( & str , & str ) ] ,
142+ ) -> CommandResult {
143+ let mut cmd = Command :: new ( & self . binary ) ;
144+ cmd. args ( args) . current_dir ( cwd. as_ref ( ) ) ;
145+ for ( k, v) in envs {
146+ cmd. env ( k, v) ;
147+ }
148+ command_output ( & mut cmd, & format ! ( "tnmsc {}" , args. join( " " ) ) )
149+ }
150+
129151 pub fn run_success ( & self , args : & [ & str ] ) -> CommandResult {
130152 let result = self . run ( args) ;
131153 result. assert_success ( & format ! ( "tnmsc {}" , args. join( " " ) ) ) ;
@@ -341,6 +363,47 @@ impl LocalTestRunner {
341363 }
342364}
343365
366+ // ---------------------------------------------------------------------------
367+ // Cross-process file lock — prevents test binaries from interfering with each
368+ // other when running local tests on the shared project directory.
369+ // ---------------------------------------------------------------------------
370+
371+ pub struct CrossProcessLock ( Option < PathBuf > ) ;
372+
373+ impl Drop for CrossProcessLock {
374+ fn drop ( & mut self ) {
375+ if let Some ( path) = self . 0 . take ( ) {
376+ let _ = std:: fs:: remove_file ( & path) ;
377+ }
378+ }
379+ }
380+
381+ fn acquire_cross_process_lock ( ) -> CrossProcessLock {
382+ let lock_path = home_dir ( ) . join ( ".tnmsc_local_test_lock" ) ;
383+ loop {
384+ match std:: fs:: File :: create_new ( & lock_path) {
385+ Ok ( _) => return CrossProcessLock ( Some ( lock_path) ) ,
386+ Err ( e) if e. kind ( ) == std:: io:: ErrorKind :: AlreadyExists => {
387+ // Stale-lock detection: if older than 5 minutes, remove and retry
388+ if let Ok ( meta) = std:: fs:: metadata ( & lock_path) {
389+ if let Ok ( created) = meta. created ( ) {
390+ if let Ok ( elapsed) = created. elapsed ( ) {
391+ if elapsed > Duration :: from_secs ( 300 ) {
392+ let _ = std:: fs:: remove_file ( & lock_path) ;
393+ continue ;
394+ }
395+ }
396+ }
397+ }
398+ std:: thread:: sleep ( Duration :: from_millis ( 200 ) ) ;
399+ }
400+ Err ( _) => {
401+ std:: thread:: sleep ( Duration :: from_millis ( 200 ) ) ;
402+ }
403+ }
404+ }
405+ }
406+
344407pub fn ensure_binary ( ) {
345408 let binary = binary_path ( ) ;
346409
0 commit comments