Skip to content

Commit 4017f2a

Browse files
committed
[v0.0.5] 2025-08-27
- Helios native C++ had several bugs that was causing errors in the last version. Merged in patched version of 1.3.46. ## Visualizer - Fixed some issues that could cause the visualizer tests to crash in headless mode.
1 parent 49e0bee commit 4017f2a

7 files changed

Lines changed: 86 additions & 41 deletions

File tree

.github/workflows/docs.yml

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
name: Build and Deploy Documentation
22

3-
on:
3+
"on":
44
push:
55
branches: [ "master", "main" ]
66
pull_request:
77
branches: [ "master", "main" ]
88
types: [opened, synchronize]
99
# Allow manual trigger
10-
workflow_dispatch:
10+
workflow_dispatch: {}
1111

1212
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
1313
permissions:
@@ -56,9 +56,8 @@ jobs:
5656
5757
- name: Sync version to Doxygen config
5858
run: |
59-
# Get version from setuptools-scm and update Doxyfile
60-
export PYHELIOS_DEV_MODE=1
61-
VERSION=$(python -c "from pyhelios._version import __version__; print(__version__)")
59+
# Get version directly from _version.py to avoid importing PyHelios
60+
VERSION=$(python -c "import sys; sys.path.insert(0, 'pyhelios'); from _version import __version__; print(__version__)")
6261
echo "Updating Doxygen PROJECT_NUMBER to: $VERSION"
6362
sed -i "s/^PROJECT_NUMBER.*$/PROJECT_NUMBER = $VERSION/" docs/Doxyfile.python
6463
@@ -128,9 +127,8 @@ jobs:
128127
129128
- name: Sync version to Doxygen config (preview)
130129
run: |
131-
# Get version from setuptools-scm and update Doxyfile
132-
export PYHELIOS_DEV_MODE=1
133-
VERSION=$(python -c "from pyhelios._version import __version__; print(__version__)")
130+
# Get version directly from _version.py to avoid importing PyHelios
131+
VERSION=$(python -c "import sys; sys.path.insert(0, 'pyhelios'); from _version import __version__; print(__version__)")
134132
echo "Updating Doxygen PROJECT_NUMBER to: $VERSION"
135133
sed -i "s/^PROJECT_NUMBER.*$/PROJECT_NUMBER = $VERSION/" docs/Doxyfile.python
136134

.github/workflows/pytest-linux-gpu.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
fi
7272
7373
- name: Upload test logs
74-
uses: actions/upload-artifact@v3
74+
uses: actions/upload-artifact@v4
7575
if: always()
7676
with:
7777
name: pyhelios-gpu-test-logs

docs/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
# [v0.0.5] 2025-08-27
4+
5+
- Helios native C++ had several bugs that was causing errors in the last version. Merged in patched version of 1.3.46.
6+
7+
## Visualizer
8+
- Fixed some issues that could cause the visualizer tests to crash in headless mode.
9+
310
# [v0.0.4] 2025-08-25
411

512
🚨++ New Plug-in Integrated ++ 🚨

helios-core

Submodule helios-core updated 44 files

native/src/pyhelios_wrapper_visualizer.cpp

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,31 @@ extern "C" {
1818
//=============================================================================
1919

2020
Visualizer* createVisualizer(unsigned int width, unsigned int height, bool headless) {
21-
// Enable window decorations by default (true), headless parameter controls window visibility
22-
return new Visualizer(width, height, 4, true, headless); // 4 antialiasing samples, decorations enabled
21+
try {
22+
clearError();
23+
// Enable window decorations by default (true), headless parameter controls window visibility
24+
return new Visualizer(width, height, 4, true, headless); // 4 antialiasing samples, decorations enabled
25+
} catch (const std::exception& e) {
26+
setError(PYHELIOS_ERROR_RUNTIME, std::string("ERROR (createVisualizer): Failed to create visualizer: ") + e.what());
27+
return nullptr;
28+
} catch (...) {
29+
setError(PYHELIOS_ERROR_UNKNOWN, "ERROR (createVisualizer): Unknown error creating visualizer. This may indicate OpenGL/graphics initialization problems in headless environments.");
30+
return nullptr;
31+
}
2332
}
2433

2534
Visualizer* createVisualizerWithAntialiasing(unsigned int width, unsigned int height, unsigned int samples, bool headless) {
26-
// Enable window decorations by default (true), headless parameter controls window visibility
27-
return new Visualizer(width, height, samples, true, headless);
35+
try {
36+
clearError();
37+
// Enable window decorations by default (true), headless parameter controls window visibility
38+
return new Visualizer(width, height, samples, true, headless);
39+
} catch (const std::exception& e) {
40+
setError(PYHELIOS_ERROR_RUNTIME, std::string("ERROR (createVisualizerWithAntialiasing): Failed to create visualizer: ") + e.what());
41+
return nullptr;
42+
} catch (...) {
43+
setError(PYHELIOS_ERROR_UNKNOWN, "ERROR (createVisualizerWithAntialiasing): Unknown error creating visualizer. This may indicate OpenGL/graphics initialization problems in headless environments.");
44+
return nullptr;
45+
}
2846
}
2947

3048
void destroyVisualizer(Visualizer* visualizer) {

pyhelios/wrappers/UVisualizerWrapper.py

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,26 @@ class UVisualizer(ctypes.Structure):
2323
# Visualizer creation and destruction
2424
helios_lib.createVisualizer.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool]
2525
helios_lib.createVisualizer.restype = ctypes.POINTER(UVisualizer)
26+
27+
# Add errcheck to automatically handle errors and nulls
28+
def _check_visualizer_creation(result, func, args):
29+
if _ERROR_MANAGEMENT_AVAILABLE:
30+
check_helios_error(helios_lib.getLastError, helios_lib.getLastErrorMessage)
31+
if not result:
32+
raise RuntimeError(
33+
"Failed to create Visualizer. This may indicate:\n"
34+
"1. OpenGL/graphics initialization problems in headless environments\n"
35+
"2. Missing graphics drivers or display support\n"
36+
"3. Insufficient system resources for graphics context creation\n"
37+
"4. XQuartz or display server configuration issues on macOS/Linux"
38+
)
39+
return result
40+
41+
helios_lib.createVisualizer.errcheck = _check_visualizer_creation
2642

2743
helios_lib.createVisualizerWithAntialiasing.argtypes = [ctypes.c_uint32, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_bool]
2844
helios_lib.createVisualizerWithAntialiasing.restype = ctypes.POINTER(UVisualizer)
45+
helios_lib.createVisualizerWithAntialiasing.errcheck = _check_visualizer_creation
2946

3047
helios_lib.destroyVisualizer.argtypes = [ctypes.POINTER(UVisualizer)]
3148
helios_lib.destroyVisualizer.restype = None
@@ -118,25 +135,21 @@ def create_visualizer(width: int, height: int, headless: bool = False) -> Option
118135
)
119136

120137
try:
138+
# The errcheck callback will handle error checking and null pointer validation
121139
visualizer = helios_lib.createVisualizer(
122140
ctypes.c_uint32(width),
123141
ctypes.c_uint32(height),
124142
ctypes.c_bool(headless)
125143
)
126-
_check_for_helios_error()
127-
128-
if not visualizer:
129-
raise RuntimeError("Failed to create Visualizer")
144+
return visualizer
130145
except OSError as e:
131146
# Handle low-level system errors (e.g., graphics context failures)
132147
raise RuntimeError(
133-
f"Visualizer plugin failed to initialize: {e}. "
148+
f"Visualizer plugin failed to initialize due to system error: {e}. "
134149
"This may indicate missing graphics drivers, OpenGL issues, or "
135150
"incompatible system configuration. Try building with different options "
136151
"or check system requirements."
137152
)
138-
139-
return visualizer
140153

141154
def create_visualizer_with_antialiasing(width: int, height: int, antialiasing_samples: int, headless: bool = False) -> Optional[ctypes.POINTER(UVisualizer)]:
142155
"""
@@ -161,18 +174,23 @@ def create_visualizer_with_antialiasing(width: int, height: int, antialiasing_sa
161174
"Rebuild with visualizer plugin enabled."
162175
)
163176

164-
visualizer = helios_lib.createVisualizerWithAntialiasing(
165-
ctypes.c_uint32(width),
166-
ctypes.c_uint32(height),
167-
ctypes.c_uint32(antialiasing_samples),
168-
ctypes.c_bool(headless)
169-
)
170-
_check_for_helios_error()
171-
172-
if not visualizer:
173-
raise RuntimeError("Failed to create Visualizer with antialiasing")
174-
175-
return visualizer
177+
try:
178+
# The errcheck callback will handle error checking and null pointer validation
179+
visualizer = helios_lib.createVisualizerWithAntialiasing(
180+
ctypes.c_uint32(width),
181+
ctypes.c_uint32(height),
182+
ctypes.c_uint32(antialiasing_samples),
183+
ctypes.c_bool(headless)
184+
)
185+
return visualizer
186+
except OSError as e:
187+
# Handle low-level system errors (e.g., graphics context failures)
188+
raise RuntimeError(
189+
f"Visualizer plugin failed to initialize due to system error: {e}. "
190+
"This may indicate missing graphics drivers, OpenGL issues, or "
191+
"incompatible system configuration. Try building with different options "
192+
"or check system requirements."
193+
)
176194

177195
def destroy_visualizer(visualizer: ctypes.POINTER(UVisualizer)) -> None:
178196
"""

tests/test_visualizer.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -348,8 +348,9 @@ class TestVisualizerMockMode:
348348
def test_visualizer_mock_mode_error_messages(self):
349349
"""Test that mock mode provides helpful error messages"""
350350
import os
351+
import platform
351352
# Skip this test on macOS CI to avoid fatal crashes
352-
if os.environ.get('CI') and os.uname().sysname == 'Darwin':
353+
if os.environ.get('CI') and platform.system() == 'Darwin':
353354
pytest.skip("Skipping visualizer test on macOS CI due to graphics context issues")
354355

355356
# This test assumes we're in an environment without visualizer plugin
@@ -394,8 +395,11 @@ def test_visualizer_context_manager_protocol(self):
394395

395396
# Clean up
396397
visualizer.__exit__(None, None, None)
397-
except VisualizerError:
398-
# Expected in mock mode
398+
except (VisualizerError, RuntimeError) as e:
399+
# Expected in mock mode or when graphics initialization fails in CI
400+
# Log the specific error for debugging
401+
import sys
402+
print(f"Visualizer initialization failed as expected in CI/mock mode: {e}", file=sys.stderr)
399403
pass
400404

401405
def test_visualizer_method_signatures(self):
@@ -487,8 +491,8 @@ def visualizer_and_context(self):
487491
visualizer = Visualizer(400, 300, headless=True)
488492
visualizer.buildContextGeometry(context)
489493
yield visualizer, context, [patch1, patch2, patch3]
490-
except VisualizerError as e:
491-
pytest.skip(f"Visualizer plugin not available: {e}")
494+
except (VisualizerError, RuntimeError) as e:
495+
pytest.skip(f"Visualizer plugin not available or graphics initialization failed: {e}")
492496
finally:
493497
# Cleanup
494498
if 'visualizer' in locals():
@@ -622,13 +626,13 @@ def test_color_primitives_mock_mode_error(self):
622626
pytest.skip("Visualizer plugin available - not testing mock mode")
623627

624628
# In mock mode, visualizer creation should fail
625-
with pytest.raises(VisualizerError) as exc_info:
629+
with pytest.raises((VisualizerError, RuntimeError)) as exc_info:
626630
Visualizer(400, 300, headless=True)
627631

628632
error_msg = str(exc_info.value).lower()
629-
# Error should mention rebuilding
633+
# Error should mention rebuilding or graphics issues
630634
assert any(keyword in error_msg for keyword in
631-
['rebuild', 'build', 'enable', 'visualizer'])
635+
['rebuild', 'build', 'enable', 'visualizer', 'opengl', 'graphics', 'initialize', 'create'])
632636

633637

634638
if __name__ == "__main__":

0 commit comments

Comments
 (0)