Skip to content

Commit f8b3abf

Browse files
Pin BYOL destructive tests to Python 3.12 to stop matrix-race flakes
The CI test matrix runs 3.10, 3.11, and 3.12 in parallel against the SAME shared NDI cloud account. test_allocate_and_clear_lifecycle and test_setMatlabLicense_rejects_invalid_file both allocate then clear a MATLAB license registration; running three of them concurrently races each other's state. Observed on this branch: - 3.11 (commit 0a1d4e5): "MAC mismatch: allocate=...12:59:20:de:7c:9d ... get=...12:32:e3:a1:33:a3..." -- another job cleared and re-allocated between this run's allocate and its get. - 3.10 (commit c2921ad): CloudAPIError HTTP 500 from DELETE /users/me/matlab-license -- another job had already cleared the registration first. Rename _require_no_existing_license -> _require_destructive_safe and add a python-version gate at the top: skip on anything other than 3.12. The read-only test_getMatlabLicense keeps running on every matrix entry so the GET path is still exercised on each interpreter.
1 parent c2921ad commit f8b3abf

1 file changed

Lines changed: 35 additions & 8 deletions

File tree

tests/test_cloud_matlab_license.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,22 @@
2020
"true" -> only the read-only getMatlabLicense test runs.
2121
"false" -> destructive allocate/clear lifecycle runs end-to-end.
2222
unset -> module fails to import (collection error).
23+
24+
WHY THE DESTRUCTIVE TESTS ARE PINNED TO ONE PYTHON VERSION:
25+
The CI test matrix runs python 3.10, 3.11, and 3.12 in parallel against
26+
the SAME shared cloud account. allocate_and_clear_lifecycle and
27+
setMatlabLicense_rejects_invalid_file both POST a new ENI/MAC, then
28+
DELETE it; running 3 of them concurrently races their own state
29+
(observed: MAC-mismatch failures and HTTP 500s from DELETE on
30+
already-cleared registrations). We pin the destructive tests to
31+
python 3.12 only; the read-only getMatlabLicense test stays on every
32+
matrix entry so we still cross-check the GET path on each interpreter.
2333
"""
2434

2535
from __future__ import annotations
2636

2737
import os
38+
import sys
2839

2940
import pytest
3041

@@ -54,6 +65,12 @@
5465

5566
USER_HAS_EXISTING_LICENSE = user_has_existing_license()
5667

68+
# The destructive tests share a single cloud account across the python
69+
# matrix; pin them to one interpreter to avoid the 3.10/3.11/3.12 jobs
70+
# racing each other's allocate/clear cycles. The read-only test still
71+
# runs on every matrix entry.
72+
_DESTRUCTIVE_PINNED_VERSION = (3, 12)
73+
5774

5875
# ---------------------------------------------------------------------------
5976
# Fixtures
@@ -88,7 +105,7 @@ def destructive_teardown(cloud_client):
88105
89106
NEVER runs effectively when NDI_CLOUD_TEST_USER_HAS_MATLAB_LICENSE=true:
90107
in that mode the destructive tests are skipped entirely (see
91-
_require_no_existing_license) BEFORE the holder is ever flipped, so
108+
_require_destructive_safe) BEFORE the holder is ever flipped, so
92109
clear_on_teardown stays False and the teardown is a no-op. This
93110
guarantees we never call DELETE on an existing license we were
94111
supposed to preserve.
@@ -105,8 +122,16 @@ def destructive_teardown(cloud_client):
105122
pass
106123

107124

108-
def _require_no_existing_license():
109-
"""Skip a destructive test when the account has a real license to preserve."""
125+
def _require_destructive_safe():
126+
"""Skip when a destructive test would race or destroy a real license."""
127+
if sys.version_info[:2] != _DESTRUCTIVE_PINNED_VERSION:
128+
pytest.skip(
129+
"Destructive BYOL tests pinned to Python "
130+
f"{_DESTRUCTIVE_PINNED_VERSION[0]}.{_DESTRUCTIVE_PINNED_VERSION[1]} "
131+
"to avoid parallel-matrix interference on the shared cloud account "
132+
"(allocate/clear races between python versions produce flaky "
133+
"MAC-mismatch and HTTP 500 failures)."
134+
)
110135
if USER_HAS_EXISTING_LICENSE:
111136
pytest.skip(
112137
"Skipped: NDI_CLOUD_TEST_USER_HAS_MATLAB_LICENSE=true. "
@@ -124,7 +149,7 @@ class TestMatlabLicense:
124149
"""Mirror of MATLAB MatlabLicenseTest."""
125150

126151
def test_getMatlabLicense(self, cloud_client):
127-
"""Read-only check that runs in BOTH modes.
152+
"""Read-only check that runs in BOTH modes and on every python version.
128153
129154
Asserts that the server returns a sane MatlabLicenseStatus dict
130155
and that its ``files`` array agrees with the env-var declaration:
@@ -157,11 +182,12 @@ def test_getMatlabLicense(self, cloud_client):
157182
def test_allocate_and_clear_lifecycle(self, cloud_client, destructive_teardown):
158183
"""POST -> GET -> DELETE -> GET round-trip.
159184
160-
Skipped when HAS_LICENSE=true (DELETE would destroy a real
185+
Pinned to a single python version (see module docstring); also
186+
skipped when HAS_LICENSE=true (DELETE would destroy a real
161187
registration). The teardown finalizer releases the ENI we
162188
ourselves allocated if any of the intermediate asserts fail.
163189
"""
164-
_require_no_existing_license()
190+
_require_destructive_safe()
165191

166192
from ndi.cloud.api.users import (
167193
allocateMatlabLicenseMac,
@@ -206,12 +232,13 @@ def test_allocate_and_clear_lifecycle(self, cloud_client, destructive_teardown):
206232
def test_setMatlabLicense_rejects_invalid_file(self, cloud_client, destructive_teardown):
207233
"""Negative test: PUT with a bogus lic body should return HTTP 400.
208234
209-
Skipped when HAS_LICENSE=true (even a 400-rejected PUT could
235+
Pinned to a single python version (see module docstring); also
236+
skipped when HAS_LICENSE=true (even a 400-rejected PUT could
210237
disturb an existing registration if server semantics change).
211238
Requires a prior allocate so the server actually reaches file
212239
validation rather than rejecting on 'no MAC allocated'.
213240
"""
214-
_require_no_existing_license()
241+
_require_destructive_safe()
215242

216243
from ndi.cloud.api.users import (
217244
allocateMatlabLicenseMac,

0 commit comments

Comments
 (0)