Skip to content

Commit d88ece2

Browse files
committed
Skip persistent macOS-headless engine-state lag in #316 flake-prone tests
Per #316 acceptance criterion #2: after 12+ fix attempts the Camera2D `make_current` → `Viewport.camera_2d` slot propagation still occasionally lags on macOS headless. The handler-side logical-current bookkeeping (#311) stays correct in that race; only the engine-side viewport slot lags. The test polls 200 ms (#341 widened diagnostics) but observed lag has been up to ~600 ms in the wild — beyond any reasonable wait budget for a unit test. Add `_skip_if_macos_engine_lag(cam, expected, report, label)`: - if the wait already settled → noop, caller continues - if not on macOS headless → noop, caller's hard assertion fails normally - if handler logical state disagrees with expectation → noop (real regression, caller's hard assertion fires with the diagnostic) - only when on macOS headless AND handler state agrees → skip() with the full diagnostic message so the next investigator still sees the divergence Wired into `test_create_with_make_current_unmarks_sibling` and the four poll sites in `test_configure_current_sibling_unmark_single_undo`. Linux / Windows behavior is unchanged — those platforms still hard-fail on a missed propagation. This intentionally does NOT close the underlying race: it converts the known upstream-flavor flake into a soft signal so CI stops reporting noise on it. The diagnostic-rich skip message keeps the failure visible enough that any NEW divergence (e.g. a real handler regression) still surfaces. Refs: #316
1 parent 758d821 commit d88ece2

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

test_project/tests/test_camera.gd

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,56 @@ func _camera_current_diag(cam: Node, expected: bool, attempts: int, elapsed_msec
237237
]
238238

239239

240+
# Issue #316 gate. After 12+ fix attempts, the Camera2D `make_current` →
241+
# `Viewport.camera_2d` slot propagation still occasionally lags on macOS
242+
# headless (the test polls 200ms, observed up to ~600ms in the wild). The
243+
# handler-side logical bookkeeping (#311) stays correct in that race — only
244+
# the engine-side viewport slot doesn't catch up. Per the issue's acceptance
245+
# criterion #2, the macOS-only failure is gated when the handler view agrees
246+
# with the expectation: skip with the full diagnostic so the next investigator
247+
# still sees the state divergence, but don't fail the build for a known
248+
# upstream race we can't close at the plugin level.
249+
#
250+
# This deliberately does NOT skip on Linux/Windows or when the handler view
251+
# also disagrees — those would represent real regressions and must fail.
252+
func _is_macos_headless() -> bool:
253+
return OS.has_feature("macos") and DisplayServer.get_name() == "headless"
254+
255+
256+
func _handler_logical_current_matches(cam: Node, expected: bool) -> bool:
257+
if cam == null or not is_instance_valid(cam):
258+
return false
259+
var scene_root := EditorInterface.get_edited_scene_root()
260+
if scene_root == null or not scene_root.is_ancestor_of(cam):
261+
return false
262+
var cam_path := McpScenePath.from_node(cam, scene_root)
263+
var handler_view := _handler.get_camera({"camera_path": cam_path})
264+
if not handler_view.has("data"):
265+
return false
266+
return bool(handler_view.data.get("current", false)) == expected
267+
268+
269+
# Returns true and calls skip() when the test should bail out on a macOS-
270+
# headless engine-state lag the handler already agrees was settled correctly.
271+
# Returns false when the caller should proceed to its direct assertions —
272+
# either the wait succeeded, we're not on macOS headless, or the handler
273+
# also disagrees (real regression).
274+
func _skip_if_macos_engine_lag(cam: Node, expected: bool, report: Dictionary, label: String) -> bool:
275+
if report.settled:
276+
return false
277+
if not _is_macos_headless():
278+
return false
279+
if not _handler_logical_current_matches(cam, expected):
280+
return false
281+
var msg := (
282+
"Engine-state lag on macOS headless (#316): handler logical state "
283+
+ "matches expected current=%s but Viewport.camera_2d slot didn't "
284+
+ "propagate within the wait budget. %s%s"
285+
) % [expected, label, report.message]
286+
skip(msg)
287+
return true
288+
289+
240290
# ============================================================================
241291
# camera_create
242292
# ============================================================================
@@ -287,8 +337,12 @@ func test_create_with_make_current_unmarks_sibling() -> void:
287337
assert_true(first_node != null, "First camera should resolve from %s" % first.data.path)
288338
assert_true(second_node != null, "Second camera should resolve from %s" % second.data.path)
289339
var second_current := _wait_for_camera_current_report(second_node, true)
340+
if _skip_if_macos_engine_lag(second_node, true, second_current, "second after create"):
341+
return
290342
assert_true(second_current.settled, second_current.message)
291343
var first_not_current := _wait_for_camera_current_report(first_node, false)
344+
if _skip_if_macos_engine_lag(first_node, false, first_not_current, "first unmarked after sibling create"):
345+
return
292346
assert_true(first_not_current.settled, first_not_current.message)
293347
assert_eq(second_node.is_current(), true, "Direct is_current mismatch after wait succeeded. %s" % _camera_current_diag(second_node, true, second_current.attempts, second_current.elapsed_msec))
294348
assert_eq(first_node.is_current(), false, "Previously-current camera should have been unmarked. Direct is_current mismatch after wait succeeded. %s" % _camera_current_diag(first_node, false, first_not_current.attempts, first_not_current.elapsed_msec))
@@ -405,8 +459,12 @@ func test_configure_current_sibling_unmark_single_undo() -> void:
405459
})
406460
assert_has_key(result, "data")
407461
var forward_second_current := _wait_for_camera_current_report(second_node, true)
462+
if _skip_if_macos_engine_lag(second_node, true, forward_second_current, "forward configure: second"):
463+
return
408464
assert_true(forward_second_current.settled, forward_second_current.message)
409465
var forward_first_not_current := _wait_for_camera_current_report(first_node, false)
466+
if _skip_if_macos_engine_lag(first_node, false, forward_first_not_current, "forward configure: first unmarked"):
467+
return
410468
assert_true(forward_first_not_current.settled, forward_first_not_current.message)
411469
assert_eq(second_node.is_current(), true, "Direct is_current mismatch after forward configure wait succeeded. %s" % _camera_current_diag(second_node, true, forward_second_current.attempts, forward_second_current.elapsed_msec))
412470
assert_eq(first_node.is_current(), false, "Direct is_current mismatch after forward configure wait succeeded. %s" % _camera_current_diag(first_node, false, forward_first_not_current.attempts, forward_first_not_current.elapsed_msec))
@@ -419,8 +477,12 @@ func test_configure_current_sibling_unmark_single_undo() -> void:
419477
# Diagnostic detail if this ever regresses (#316): report viewport state,
420478
# direct Camera current state, handler/logical current reads, and wait budget.
421479
var undo_second_not_current := _wait_for_camera_current_report(second_node, false)
480+
if _skip_if_macos_engine_lag(second_node, false, undo_second_not_current, "post-undo: second unmarked"):
481+
return
422482
assert_true(undo_second_not_current.settled, undo_second_not_current.message)
423483
var undo_first_current := _wait_for_camera_current_report(first_node, true)
484+
if _skip_if_macos_engine_lag(first_node, true, undo_first_current, "post-undo: first restored"):
485+
return
424486
assert_true(undo_first_current.settled, undo_first_current.message)
425487
assert_eq(second_node.is_current(), false, "After undo second should not be current. %s" % _camera_current_diag(second_node, false, undo_second_not_current.attempts, undo_second_not_current.elapsed_msec))
426488
assert_eq(first_node.is_current(), true, "Single undo should restore original current camera. %s" % _camera_current_diag(first_node, true, undo_first_current.attempts, undo_first_current.elapsed_msec))

0 commit comments

Comments
 (0)