@@ -51,15 +51,115 @@ function is_wp_error( $thing ): bool
5151 }
5252 }
5353
54- if (! function_exists ('wp_mkdir_p ' ) ) {
55- function wp_mkdir_p ( string $ path ): bool
56- {
57- return is_dir ($ path ) || mkdir ($ path , 0755 , true );
58- }
59- }
54+ if (! function_exists ('wp_mkdir_p ' ) ) {
55+ function wp_mkdir_p ( string $ path ): bool
56+ {
57+ return is_dir ($ path ) || mkdir ($ path , 0755 , true );
58+ }
59+ }
60+
61+ if (! function_exists ('wp_json_encode ' ) ) {
62+ function wp_json_encode ( $ data , int $ flags = 0 )
63+ {
64+ return json_encode ($ data , $ flags );
65+ }
66+ }
67+
68+ class Workspace_Mutation_Lock_Test_Wpdb
69+ {
70+ public string $ prefix = 'wp_ ' ;
71+ public int $ insert_id = 0 ;
72+ public int $ rows_affected = 0 ;
73+ public string $ last_error = '' ;
74+
75+ /** @var array<int,array<string,mixed>> */
76+ public array $ rows = array ();
77+
78+ public function insert ( string $ table , array $ data , array $ format ): int // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
79+ {
80+ $ this ->insert_id ++;
81+ $ data ['id ' ] = $ this ->insert_id ;
82+ $ this ->rows [ $ this ->insert_id ] = $ data ;
83+ return 1 ;
84+ }
85+
86+ public function update ( string $ table , array $ data , array $ where , array $ format , array $ where_format ): int // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
87+ {
88+ $ id = (int ) ( $ where ['id ' ] ?? 0 );
89+ if (! isset ($ this ->rows [ $ id ]) ) {
90+ return 0 ;
91+ }
92+ $ this ->rows [ $ id ] = array_merge ($ this ->rows [ $ id ], $ data );
93+ return 1 ;
94+ }
6095
61- include __DIR__ . '/../inc/Workspace/WorkspaceLockStore.php ' ;
62- include __DIR__ . '/../inc/Workspace/WorkspaceMutationLock.php ' ;
96+ public function get_var ( string $ sql ): mixed
97+ {
98+ if (str_contains ($ sql , 'SHOW TABLES LIKE ' ) ) {
99+ return $ this ->prefix . 'datamachine_code_locks ' ;
100+ }
101+
102+ if (! str_contains ($ sql , 'COUNT(*) ' ) ) {
103+ return null ;
104+ }
105+
106+ return count ($ this ->matching_rows ($ sql ));
107+ }
108+
109+ /**
110+ * @return array<int,string>
111+ */
112+ public function get_col ( string $ sql ): array
113+ {
114+ $ keys = array_map (
115+ static fn ( array $ row ): string => (string ) ( $ row ['lock_key ' ] ?? '' ),
116+ $ this ->matching_rows ($ sql )
117+ );
118+ return array_values (array_unique (array_filter ($ keys )));
119+ }
120+
121+ public function prepare ( string $ query , mixed ...$ args ): string
122+ {
123+ foreach ( $ args as $ arg ) {
124+ $ replacement = is_int ($ arg ) ? (string ) $ arg : "' " . str_replace ("' " , "'' " , (string ) $ arg ) . "' " ;
125+ $ query = preg_replace ('/%[sd]/ ' , $ replacement , $ query , 1 ) ?? $ query ;
126+ }
127+ return $ query ;
128+ }
129+
130+ /**
131+ * @return array<int,array<string,mixed>>
132+ */
133+ private function matching_rows ( string $ sql ): array
134+ {
135+ $ now = gmdate ('Y-m-d H:i:s ' );
136+ return array_values (
137+ array_filter (
138+ $ this ->rows ,
139+ static function ( array $ row ) use ( $ sql , $ now ): bool {
140+ $ status = (string ) ( $ row ['status ' ] ?? '' );
141+ $ expiry = (string ) ( $ row ['expires_at ' ] ?? '' );
142+ if (str_contains ($ sql , "status = 'active' " ) && 'active ' !== $ status ) {
143+ return false ;
144+ }
145+ if (str_contains ($ sql , "status = 'released' " ) ) {
146+ return 'released ' === $ status ;
147+ }
148+ if (str_contains ($ sql , 'expires_at < ' ) ) {
149+ return '' !== $ expiry && $ expiry < $ now ;
150+ }
151+ if (str_contains ($ sql , 'expires_at >= ' ) ) {
152+ return '' !== $ expiry && $ expiry >= $ now ;
153+ }
154+ return true ;
155+ }
156+ )
157+ );
158+ }
159+ }
160+
161+ include __DIR__ . '/../inc/Workspace/WorkspaceLockStore.php ' ;
162+ include __DIR__ . '/../inc/Workspace/WorkspaceMutationLock.php ' ;
63163
64164 $ failures = 0 ;
65165 $ total = 0 ;
@@ -109,11 +209,23 @@ function () use ( $tmp ) {
109209 if (! is_wp_error ($ first ) ) {
110210 $ first ->release ();
111211 }
112- $ status = \DataMachineCode \Workspace \WorkspaceMutationLock::status ($ tmp );
113- $ assert (0 , (int ) $ status ['active ' ], 'released filesystem lock is no longer active ' );
114- $ assert (2 , (int ) $ status ['filesystem ' ]['recent ' ], 'released filesystem lock files are visible as recent retention residue ' );
212+ $ status = \DataMachineCode \Workspace \WorkspaceMutationLock::status ($ tmp );
213+ $ assert (0 , (int ) $ status ['active ' ], 'released filesystem lock is no longer active ' );
214+ $ assert (2 , (int ) $ status ['filesystem ' ]['recent ' ], 'released filesystem lock files are visible as recent retention residue ' );
215+
216+ $ GLOBALS ['wpdb ' ] = new Workspace_Mutation_Lock_Test_Wpdb ();
217+ $ db_backed = \DataMachineCode \Workspace \WorkspaceMutationLock::acquire ($ tmp , 'db-demo ' , 1 );
218+ $ assert (false , is_wp_error ($ db_backed ), 'DB-backed acquisition succeeds ' );
219+ $ db_backed_status = \DataMachineCode \Workspace \WorkspaceMutationLock::status ($ tmp );
220+ $ assert (1 , (int ) $ db_backed_status ['database ' ]['active ' ], 'DB-backed acquisition records one active DB row ' );
221+ $ assert (1 , (int ) $ db_backed_status ['filesystem ' ]['active ' ], 'DB-backed acquisition also holds one filesystem lock ' );
222+ $ assert (1 , (int ) $ db_backed_status ['active ' ], 'same DB row and filesystem lock count as one logical active lock ' );
223+ if (! is_wp_error ($ db_backed ) ) {
224+ $ db_backed ->release ();
225+ }
226+ unset($ GLOBALS ['wpdb ' ]);
115227
116- $ stale_path = $ tmp . '/.locks/worktree-stale.lock ' ;
228+ $ stale_path = $ tmp . '/.locks/worktree-stale.lock ' ;
117229 touch ($ stale_path , time () - 172800 );
118230 $ stale_status = \DataMachineCode \Workspace \WorkspaceMutationLock::status ($ tmp );
119231 $ assert (1 , (int ) $ stale_status ['stale ' ], 'old unlocked filesystem lock is counted stale ' );
0 commit comments