@@ -152,18 +152,27 @@ func TestKeyboardLayouts_ListsTheEnabledLayoutsAndTheReference(t *testing.T) {
152152// the main queue stalls. Resolving the layout calls TIS, which must run on the
153153// main thread, so the apply waits in the queue and runs once the main thread
154154// is free. The call returns true meanwhile, because a layout not yet applied
155- // has not failed to resolve. An ID no layout has makes the apply visible on
156- // any machine, since it flips the unmatched flag KeyboardLayouts reports.
155+ // has not failed to resolve. KeyboardLayouts reports the queued layout as not
156+ // unmatched too. The previous layout's outcome would fail doctor for a valid
157+ // replacement, and an outcome for the new one would mean TIS ran off the main
158+ // thread. An ID no layout has makes either mistake visible on any machine,
159+ // since it flips the unmatched flag KeyboardLayouts reports.
157160func TestSetReferenceKeyboardLayout_LeavesTheApplyQueuedWhenTheMainQueueIsBlocked (t * testing.T ) {
158161 const missingLayout = "com.example.keylayout.NoSuchLayout"
159162
163+ _ , enabledLayout , _ := darwin .KeyboardLayouts ()
164+ if enabledLayout == "" {
165+ t .Fatal ("KeyboardLayouts named no reference layout" )
166+ }
167+
160168 tests := []struct {
161169 name string
162170 prior string
163171 next string
164172 }{
165173 {name : "missing layout after auto" , prior : "" , next : missingLayout },
166174 {name : "auto after missing layout" , prior : missingLayout , next : "" },
175+ {name : "enabled layout after missing layout" , prior : missingLayout , next : enabledLayout },
167176 }
168177
169178 for _ , test := range tests {
@@ -172,9 +181,8 @@ func TestSetReferenceKeyboardLayout_LeavesTheApplyQueuedWhenTheMainQueueIsBlocke
172181
173182 darwin .SetReferenceKeyboardLayout (test .prior )
174183
175- _ , _ , priorUnmatched := darwin .KeyboardLayouts ()
176- if priorUnmatched != (test .prior != "" ) {
177- t .Fatalf ("unmatched is %v after setting %q" , priorUnmatched , test .prior )
184+ if _ , _ , unmatched := darwin .KeyboardLayouts (); unmatched != (test .prior == missingLayout ) {
185+ t .Fatalf ("unmatched is %v after setting %q" , unmatched , test .prior )
178186 }
179187
180188 release := darwin .BlockMainQueueForTesting ()
@@ -194,24 +202,99 @@ func TestSetReferenceKeyboardLayout_LeavesTheApplyQueuedWhenTheMainQueueIsBlocke
194202 t .Fatalf ("SetReferenceKeyboardLayout waited %v for the blocked main queue" , elapsed )
195203 }
196204
197- if _ , _ , unmatched := darwin .KeyboardLayouts (); unmatched != priorUnmatched {
198- t .Fatal ("layout was applied off the main thread while the main queue was blocked " )
205+ if _ , _ , unmatched := darwin .KeyboardLayouts (); unmatched {
206+ t .Fatal ("KeyboardLayouts reported a layout still queued as unmatched " )
199207 }
200208
201209 release ()
202210
211+ // A read that lists layouts ran on the main queue after the apply
212+ // queued before it, so its unmatched flag is the applied outcome.
203213 deadline := time .Now ().Add (5 * time .Second )
204214 for {
205- if _ , _ , unmatched := darwin .KeyboardLayouts (); unmatched != priorUnmatched {
215+ names , _ , unmatched := darwin .KeyboardLayouts ()
216+ if len (names ) != 0 {
217+ if want := test .next == missingLayout ; unmatched != want {
218+ t .Fatalf (
219+ "unmatched is %v after %q applied, want %v" ,
220+ unmatched ,
221+ test .next ,
222+ want ,
223+ )
224+ }
225+
206226 return
207227 }
208228
209229 if time .Now ().After (deadline ) {
210- t .Fatal ("queued layout was never applied after the main queue drained " )
230+ t .Fatal ("KeyboardLayouts read nothing after the main queue was released " )
211231 }
212232
213233 time .Sleep (50 * time .Millisecond )
214234 }
215235 })
216236 }
217237}
238+
239+ // TestKeyboardLayouts_ReadsNothingOffTheMainThreadWhenTheMainQueueIsBlocked
240+ // pins that a caller off the main thread never reads the layouts through TIS
241+ // itself, which must run on the main thread. neru doctor and the health check
242+ // call it from the IPC goroutine. While the main queue is blocked it returns no
243+ // names and no reference, which doctor shows as not read yet. The unmatched
244+ // flag is the one the resolver last recorded. Only a TIS call produces the
245+ // names, so an empty list shows none ran here. Once the test releases the
246+ // queue, KeyboardLayouts reads the layouts again.
247+ func TestKeyboardLayouts_ReadsNothingOffTheMainThreadWhenTheMainQueueIsBlocked (t * testing.T ) {
248+ _ , _ , wantUnmatched := darwin .KeyboardLayouts ()
249+
250+ release := darwin .BlockMainQueueForTesting ()
251+ t .Cleanup (release )
252+
253+ type layouts struct {
254+ names []string
255+ reference string
256+ unmatched bool
257+ }
258+
259+ result := make (chan layouts , 1 )
260+
261+ go func () {
262+ names , reference , unmatched := darwin .KeyboardLayouts ()
263+ result <- layouts {names : names , reference : reference , unmatched : unmatched }
264+ }()
265+
266+ var blocked layouts
267+
268+ select {
269+ case blocked = <- result :
270+ case <- time .After (2 * time .Second ):
271+ t .Fatal ("KeyboardLayouts waited for the blocked main queue" )
272+ }
273+
274+ if len (blocked .names ) != 0 || blocked .reference != "" {
275+ t .Fatalf (
276+ "KeyboardLayouts read %v and %q off the main thread" ,
277+ blocked .names ,
278+ blocked .reference ,
279+ )
280+ }
281+
282+ if blocked .unmatched != wantUnmatched {
283+ t .Fatalf (
284+ "unmatched is %v while the main queue is blocked, want %v" ,
285+ blocked .unmatched ,
286+ wantUnmatched ,
287+ )
288+ }
289+
290+ release ()
291+
292+ names , reference , _ := darwin .KeyboardLayouts ()
293+ if len (names ) == 0 {
294+ t .Fatal ("KeyboardLayouts read nothing after the main queue was released" )
295+ }
296+
297+ if ! slices .Contains (names , reference ) {
298+ t .Fatalf ("reference layout %q is not among the enabled layouts %v" , reference , names )
299+ }
300+ }
0 commit comments