Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/actions/build/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ runs:
requirements_dev.txt
requirements_doc.txt
requirements.txt
.github/workflows/requirements_dev.txt
.github/workflows/requirements_doc.txt
.github/workflows/requirements.txt

- name: Setup system dependencies
uses: ./.github/actions/setup
Expand Down
4 changes: 1 addition & 3 deletions .github/workflows/requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ conan
packaging
setuptools
setuptools-scm
# TODO: The SWIG 4.4.X is incompatible with Basilisk. Pin to SWIG 4.3.1 until
# Basilisk is updated to support SWIG 4.4.X
swig<=4.3.1,>=4.2.1
swig
libclang

pytest
Expand Down
10 changes: 5 additions & 5 deletions docs/source/Support/bskKnownIssues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ Basilisk Known Issues

Version |release|
-----------------
- When building from source on Python 3.13 using SWIG 4.4.0, a build failure may occur
if ``pyLimitedAPI`` is set to an ABI lower than Python 3.13 (e.g., ``0x03080000``).
SWIG 4.4.0 introduces a new C-API codepath for Python 3.13 that expects newer
definition macros which are not present when targeting older ``abi3`` compatibility. As such, when building
Basilisk with Python 3.13 or above, we automatically default to using the newer cp313 ABI.
- SWIG 4.4.0 caused Basilisk build failures in some Python 3.13+ source-build configurations.
The development dependency range now excludes SWIG 4.4.0, and SWIG 4.4.1 has been verified to build
successfully. If source builds fail with SWIG 4.4.0 or emit ``builtin type swigvarlink has no __module__ attribute``
warnings, upgrade to SWIG 4.4.1 or newer.


Version 2.10.0 (April 2, 2026)
------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Updated SWIG dependency support to allow compatible latest SWIG releases while excluding SWIG 4.4.0 due to compile regressions observed during testing. If you are getting lots of ``builtin type swigvarlink has no __module__ attribute`` warnings upgrade to SWIG 4.4.1
- Cleaned up pytest resource handling so parallel test runs with pytest-xdist and pytest-rerunfailures no longer report unclosed socket warnings.
7 changes: 1 addition & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ requires = [
# Requirements for building Basilisk through conanfile
"conan>=2.0.5,<=2.23.0",
"cmake>=3.26,<4.0",
"swig>=4.2.1,<=4.3.1", # Known to work with https://github.com/nightlark/swig-pypi/pull/120
"swig>=4.2.1,!=4.4.0,<5", # Support Basilisk's established SWIG baseline while avoiding the known-bad 4.4.0 release.
"libclang>=15.0.6.1,<=18.1.1" # Required by generatePayloadMetaJson.py (message code generation)
]

Expand Down Expand Up @@ -105,8 +105,3 @@ markers = [
"scenarioTest: mark a test as a tutorial scenario test.",
"ciSkip: mark a test that should be skipped on the CI test runs",
]
filterwarnings = [
"ignore:builtin type SwigPyPacked has no __module__ attribute:DeprecationWarning",
"ignore:builtin type SwigPyObject has no __module__ attribute:DeprecationWarning",
"ignore:builtin type swigvarlink has no __module__ attribute:DeprecationWarning",
]
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ conan>=2.0.5,<=2.23.0
packaging>=24,<26
setuptools>=70.1.0,<=80.9.0
setuptools-scm>=8.0,<=9.2.2
swig<=4.3.1,>=4.2.1
swig>=4.2.1,!=4.4.0,<5
libclang>=15.0.6.1,<=18.1.1

pytest>=8.3.5,<=9.0.1
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def finalize_options(self) -> None:

# Set limited ABI compatibility by default, targeting the minimum required Python version.
# See https://docs.python.org/3/c-api/stable.html
# NOTE: Swig 4.2.1 or higher is required, see https://github.com/swig/swig/pull/2727
# NOTE: Swig 4.2.1 or higher is required. Newer 4.4.x releases avoid deprecated wrapper warnings on newer Python.
min_version = next(self.distribution.python_requires.filter([f"3.{i}" for i in range(2, 100)])).replace(".", "")
wheel_py_limited = f"cp{min_version}"
bdist_wheel = self.reinitialize_command("bdist_wheel", py_limited_api=wheel_py_limited)
Expand Down
2 changes: 1 addition & 1 deletion src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ if(PY_LIMITED_API AND NOT PY_LIMITED_API STREQUAL "")
# compatibility (basically, we can build a single Python wheel to
# support all future Python versions).
# See https://docs.python.org/3/c-api/stable.html
# NOTE: Swig 4.2.0 is required, see https://github.com/swig/swig/pull/2727
# NOTE: Swig 4.2.0 is required. Swig 4.4.x avoids the deprecated wrapper warnings on newer Python releases.
add_definitions("-DPy_LIMITED_API=${PY_LIMITED_API}") # Support for current Python version

# Force libraries to link to the Stable ABI module instead.
Expand Down
54 changes: 46 additions & 8 deletions src/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,12 @@
import shutil
import subprocess
import sys
import warnings
from datetime import date

import matplotlib as mpl
import matplotlib.pyplot as plt
import pytest

warnings.filterwarnings(
"ignore",
message="builtin type swigvarlink has no __module__ attribute",
category=DeprecationWarning,
)

SHOW_PLOTS_REMOVAL_DATE = date(2027, 2, 12)
SHOW_PLOTS_DEPRECATION_MESSAGE = (
"The pytest option '--show_plots' is deprecated and will be removed after February 12, 2027."
Expand All @@ -42,6 +35,39 @@
"The pytest option '--show_plots' has been deprecated for a year and will be removed shortly."
)


def _patch_rerunfailures_socket_cleanup():
"""
Close pytest-rerunfailures server-side sockets when xdist is active.

pytest-rerunfailures 16.1 opens localhost sockets to coordinate reruns
between xdist workers, but accepted server connections are not closed by
the plugin. This narrow patch preserves the plugin handler while ensuring
each accepted connection is closed when the handler exits.
"""
try:
import pytest_rerunfailures
except ImportError:
return

server_status_db = getattr(pytest_rerunfailures, "ServerStatusDB", None)
if server_status_db is None:
return
if getattr(server_status_db, "_bsk_socket_cleanup_patched", False):
return

original_run_connection = server_status_db.run_connection

def run_connection_with_socket_close(self, conn):
with conn:
return original_run_connection(self, conn)

server_status_db.run_connection = run_connection_with_socket_close
server_status_db._bsk_socket_cleanup_patched = True


_patch_rerunfailures_socket_cleanup()

filename = inspect.getframeinfo(inspect.currentframe()).filename
path = os.path.dirname(os.path.abspath(filename))
print(path)
Expand Down Expand Up @@ -78,6 +104,17 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
terminalreporter.write_line(message, **{terminal_color: True}, bold=True)


def pytest_unconfigure(config):
failures_db = getattr(config, "failures_db", None)
socket_handle = getattr(failures_db, "sock", None)
if socket_handle is None:
return
try:
socket_handle.close()
except OSError:
pass


@pytest.fixture(scope="module")
def show_plots(request):
return request.config.getoption("--show_plots")
Expand Down Expand Up @@ -116,4 +153,5 @@ def reset_matplotlib_state():
quit()

if 'pytest-html' in installed_packages:
exec(open(path + "/reportconf.py").read(), globals())
with open(path + "/reportconf.py") as reportConfig:
exec(reportConfig.read(), globals())
5 changes: 0 additions & 5 deletions src/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,3 @@ markers =
slowtest: mark a test as a slow unit test.
scenarioTest: mark a test as a tutorial scenario test.
ciSkip: mark a test that should be skipped on the CI test runs

filterwarnings =
ignore:builtin type SwigPyPacked has no __module__ attribute:DeprecationWarning
ignore:builtin type SwigPyObject has no __module__ attribute:DeprecationWarning
ignore:builtin type swigvarlink has no __module__ attribute:DeprecationWarning
20 changes: 0 additions & 20 deletions src/simulation/environment/spaceWeatherData/spaceWeatherData.i
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,6 @@
#include "spaceWeatherData.h"
%}

%pythonbegin %{
import warnings as _warnings

_warnings.filterwarnings(
"ignore",
message=r"builtin type SwigPyPacked has no __module__ attribute",
category=DeprecationWarning,
)
_warnings.filterwarnings(
"ignore",
message=r"builtin type SwigPyObject has no __module__ attribute",
category=DeprecationWarning,
)
_warnings.filterwarnings(
"ignore",
message=r"builtin type swigvarlink has no __module__ attribute",
category=DeprecationWarning,
)
%}

%pythoncode %{
from Basilisk.architecture.swig_common_model import *
%}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,59 @@ def _runTestWithTimeout(resultQueue, numWorkers, iterationsPerWorker):
False,
)
)
finally:
_closeQueue(resultQueue)


def _closeQueue(resultQueue):
"""
Close a multiprocessing queue and wait for its feeder thread.

Parameters
----------
resultQueue : multiprocessing.Queue
Queue to close after all expected data has been read or written.
"""
try:
resultQueue.close()
except (OSError, ValueError):
pass
try:
resultQueue.join_thread()
except (AssertionError, OSError, ValueError):
pass


def _closeProcess(testProcess):
"""
Release multiprocessing process resources after the process exits.

Parameters
----------
testProcess : multiprocessing.Process
Process object to close after it has been joined.
"""
try:
testProcess.close()
except (OSError, ValueError):
pass


def _terminateProcess(testProcess):
"""
Stop a multiprocessing process and wait for shutdown.

Parameters
----------
testProcess : multiprocessing.Process
Process object to terminate.
"""
testProcess.terminate()
shutdownTimeout = 1 # [s]
testProcess.join(shutdownTimeout)
if testProcess.is_alive():
os.kill(testProcess.pid, 9)
testProcess.join(shutdownTimeout)


@pytest.mark.flaky(reruns=3)
Expand Down Expand Up @@ -260,34 +313,37 @@ def testSpiceThreadSafety(numWorkers, iterationsPerWorker):
target=_runTestWithTimeout,
args=(resultQueue, numWorkers, iterationsPerWorker),
)
testProcess.start()
try:
testProcess.start()

timeoutSeconds = 60
testProcess.join(timeoutSeconds)
timeoutSeconds = 60 # [s]
testProcess.join(timeoutSeconds)

if testProcess.is_alive():
# Hard timeout: kill the worker process and fail the test
testProcess.terminate()
testProcess.join(1)
if testProcess.is_alive():
os.kill(testProcess.pid, 9)
pytest.fail(f"Thread safety test timed out after {timeoutSeconds} seconds")

try:
results, success = resultQueue.get(block=False)

if isinstance(results, dict) and "error" in results:
# Hard timeout: kill the worker process and fail the test
_terminateProcess(testProcess)
pytest.fail(
"Thread safety test failed with error: "
f"{results['error']}\n{results.get('traceback')}"
f"Thread safety test timed out after {timeoutSeconds} seconds"
)

assert success, "Thread safety test reported thread-safety issues"
assert results["failedIterations"] == 0, (
"Some iterations failed in the thread-safety test"
)
except queue.Empty:
pytest.fail("Thread safety test completed but did not return any results")
try:
results, success = resultQueue.get(block=False)

if isinstance(results, dict) and "error" in results:
pytest.fail(
"Thread safety test failed with error: "
f"{results['error']}\n{results.get('traceback')}"
)

assert success, "Thread safety test reported thread-safety issues"
assert results["failedIterations"] == 0, (
"Some iterations failed in the thread-safety test"
)
except queue.Empty:
pytest.fail("Thread safety test completed but did not return any results")
finally:
_closeQueue(resultQueue)
_closeProcess(testProcess)


if __name__ == "__main__":
Expand All @@ -307,28 +363,29 @@ def testSpiceThreadSafety(numWorkers, iterationsPerWorker):
target=_runTestWithTimeout,
args=(resultQueue, numWorkers, iterationsPerWorker),
)
testProcess.start()
try:
testProcess.start()

timeoutSeconds = 60
testProcess.join(timeoutSeconds)
timeoutSeconds = 60 # [s]
testProcess.join(timeoutSeconds)

if testProcess.is_alive():
testProcess.terminate()
testProcess.join(1)
if testProcess.is_alive():
os.kill(testProcess.pid, 9)
print(f"ERROR: Thread safety test timed out after {timeoutSeconds} seconds")
sys.exit(2)
_terminateProcess(testProcess)
print(f"ERROR: Thread safety test timed out after {timeoutSeconds} seconds")
sys.exit(2)

try:
results, success = resultQueue.get(block=False)
try:
results, success = resultQueue.get(block=False)

if isinstance(results, dict) and "error" in results:
print(f"ERROR: Thread safety test failed with error: {results['error']}")
print(results.get("traceback"))
sys.exit(1)
if isinstance(results, dict) and "error" in results:
print(f"ERROR: Thread safety test failed with error: {results['error']}")
print(results.get("traceback"))
sys.exit(1)

sys.exit(0 if success else 1)
except queue.Empty:
print("ERROR: Thread safety test completed but did not return results")
sys.exit(1)
sys.exit(0 if success else 1)
except queue.Empty:
print("ERROR: Thread safety test completed but did not return results")
sys.exit(1)
finally:
_closeQueue(resultQueue)
_closeProcess(testProcess)
Loading