Skip to content

Commit a0b44c0

Browse files
committed
optimal Memory CICD
1 parent 8e50419 commit a0b44c0

6 files changed

Lines changed: 110 additions & 49 deletions

File tree

.github/workflows/ci-cd.yml

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,26 +153,50 @@ jobs:
153153
154154
# Set up test environment
155155
export PYTHONDONTWRITEBYTECODE=1
156+
export PYTHONHASHSEED=0
156157
export SPEECH_RECOGNITION_VENV='/opt/ros_python_env'
157158
export AI_VENV='/opt/ros_python_env'
159+
# Reduce Python memory footprint
160+
export MALLOC_TRIM_THRESHOLD_=100000
161+
export MALLOC_MMAP_THRESHOLD_=131072
158162
159163
# Run tests with coverage for Python packages (run each package separately)
160164
echo '🧪 Running tests with coverage...'
161165
PY_PACKAGES_LIST='${{ env.PY_PACKAGES }}'
162166
163167
for pkg in \${PY_PACKAGES_LIST}; do
164168
echo \"Testing package: \$pkg\"
169+
170+
# Force garbage collection before each package
171+
python3 -c \"import gc; gc.collect()\" 2>/dev/null || true
172+
173+
# Run tests with memory-optimized settings
165174
colcon test \
166175
--packages-select \$pkg \
167176
--event-handlers console_direct+ \
168-
--pytest-args --cov=\$pkg --cov-report=html --cov-report=term --cov-report=lcov --cov-report=xml:coverage.xml \
177+
--pytest-args \
178+
-p no:cacheprovider \
179+
--cov=\$pkg \
180+
--cov-report=html \
181+
--cov-report=term \
182+
--cov-report=lcov \
183+
--cov-report=xml:coverage.xml \
169184
--return-code-on-test-failure || true
170185
171186
# Copy lcov file immediately after each package test
172187
if [ -f \"build/\${pkg}/coverage.lcov\" ]; then
173188
cp \"build/\${pkg}/coverage.lcov\" /ci_workspace/coverage-py-\${pkg}.lcov
174189
echo \"✅ Copied build/\${pkg}/coverage.lcov → /ci_workspace/coverage-py-\${pkg}.lcov\"
175190
fi
191+
192+
# Clean up test artifacts to free memory between packages
193+
echo \"🧹 Cleaning up after \${pkg} tests...\"
194+
rm -rf build/\${pkg}/.pytest_cache 2>/dev/null || true
195+
rm -rf build/\${pkg}/__pycache__ 2>/dev/null || true
196+
python3 -c \"import gc; gc.collect()\" 2>/dev/null || true
197+
198+
# Brief pause to allow memory to be freed
199+
sleep 2
176200
done
177201
178202
# Copy build artifacts back to mounted directory for artifact upload

Docker/quick_test_coverage.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,9 @@ if [ ${#ALL_PKG_LIST[@]} -gt 0 ]; then
304304
for pkg in "${PY_PACKAGES[@]}"; do
305305
PYTEST_ARGS_ARRAY+=("--cov=$pkg")
306306
done
307+
# Memory-optimized settings to prevent OOM (Error 137)
307308
PYTEST_ARGS_ARRAY+=("--cov-report=html" "--cov-report=term" "--cov-report=xml")
309+
PYTEST_ARGS_ARRAY+=("-p" "no:cacheprovider") # Disable caching to reduce memory
308310
fi
309311

310312
# Convert array to proper string for colcon

src/audio_stream_manager/pytest.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ testpaths = test
33
python_files = test_*.py
44
python_classes = Test*
55
python_functions = test_*
6-
addopts = -v --tb=short
6+
# Memory-optimized settings to prevent OOM (Error 137)
7+
addopts = -v --tb=short -p no:logging -p no:warnings --maxfail=50
78
markers =
89
unit: Unit tests
910
integration: Integration tests

src/audio_stream_manager/test/conftest.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
Test configuration for audio_stream_manager package.
33
Ensures proper Python environment setup for audio dependencies.
44
"""
5+
6+
import gc
57
import os
68
import sys
7-
import pytest
89

10+
import pytest
911

1012
# Setup environment for audio_stream_manager (uses ros_python_env)
1113
VENV_PATH = os.environ.get("AI_VENV", "/opt/ros_python_env")
@@ -26,28 +28,28 @@
2628
def setup_audio_mocks():
2729
"""Setup mock dependencies for audio testing"""
2830
from unittest.mock import MagicMock
29-
31+
3032
# Mock sounddevice if not available
3133
try:
3234
import sounddevice
3335
except ImportError:
34-
sys.modules['sounddevice'] = MagicMock()
36+
sys.modules["sounddevice"] = MagicMock()
3537
print("[pytest] Mocked sounddevice")
36-
38+
3739
# Mock numpy if not available (should be available but just in case)
3840
try:
3941
import numpy
4042
except ImportError:
41-
sys.modules['numpy'] = MagicMock()
43+
sys.modules["numpy"] = MagicMock()
4244
print("[pytest] Mocked numpy")
4345

4446
# Mock ROS if not available
4547
try:
4648
import rclpy
4749
except ImportError:
48-
sys.modules['rclpy'] = MagicMock()
49-
sys.modules['rclpy.node'] = MagicMock()
50-
sys.modules['rclpy.parameter'] = MagicMock()
50+
sys.modules["rclpy"] = MagicMock()
51+
sys.modules["rclpy.node"] = MagicMock()
52+
sys.modules["rclpy.parameter"] = MagicMock()
5153
print("[pytest] Mocked rclpy")
5254

5355

@@ -67,9 +69,23 @@ def setup_tests():
6769
# Basic pytest configuration
6870
def pytest_configure(config):
6971
"""Configure pytest for audio stream manager tests"""
70-
config.addinivalue_line(
71-
"markers", "unit: Unit tests for audio components"
72-
)
73-
config.addinivalue_line(
74-
"markers", "integration: Integration tests with ROS"
75-
)
72+
config.addinivalue_line("markers", "unit: Unit tests for audio components")
73+
config.addinivalue_line("markers", "integration: Integration tests with ROS")
74+
75+
76+
# Memory optimization hooks
77+
@pytest.fixture(autouse=True)
78+
def cleanup_after_test():
79+
"""Force garbage collection after each test to prevent memory buildup"""
80+
yield
81+
gc.collect()
82+
83+
84+
def pytest_runtest_teardown(item, nextitem):
85+
"""Force garbage collection between tests to free memory"""
86+
gc.collect()
87+
88+
89+
def pytest_sessionfinish(session, exitstatus):
90+
"""Final cleanup after all tests complete"""
91+
gc.collect()

src/speech_recognition/pytest.ini

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ testpaths = test
33
python_files = test_*.py
44
python_classes = Test*
55
python_functions = test_*
6-
addopts = -v --tb=short
6+
# Memory-optimized settings to prevent OOM (Error 137)
7+
addopts = -v --tb=short -p no:logging -p no:warnings --maxfail=50
78
# Exclude weights directory and any __pycache__ from collection
89
norecursedirs = weights __pycache__ .git .pytest_cache build install log speech_recognition/weights
910
collect_ignore = ["weights", "speech_recognition/weights", "speech_recognition/weights/"]

src/speech_recognition/test/conftest.py

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
Test configuration for speech_recognition package.
33
Ensures proper Python environment setup for speech/AI dependencies.
44
"""
5+
6+
import gc
57
import os
68
import sys
79
from unittest.mock import MagicMock
810

11+
import pytest
12+
913
# Mock silero_vad.data immediately to prevent import errors during collection
1014
mock_silero_data = MagicMock()
11-
sys.modules['silero_vad.data'] = mock_silero_data
15+
sys.modules["silero_vad.data"] = mock_silero_data
1216

1317
# Setup environment for speech_recognition (uses unified ros_python_env)
1418
DIARIZATION_ENV_PATH = "/opt/ros_python_env"
@@ -28,12 +32,13 @@
2832
# Mock heavy AI/ML dependencies for testing
2933
def setup_ml_mocks():
3034
"""Setup mock dependencies for ML/AI testing without requiring full models"""
31-
from unittest.mock import MagicMock, patch
32-
35+
from unittest.mock import MagicMock
36+
3337
# Mock torch and torchaudio
3438
try:
3539
import torch
3640
import torchaudio
41+
3742
print(f"[pytest] Using real PyTorch: {torch.__version__}")
3843
except ImportError:
3944
print("[pytest] Mocking PyTorch dependencies")
@@ -43,13 +48,13 @@ def setup_ml_mocks():
4348
mock_torch.hub.load = MagicMock()
4449
mock_torch.tensor = MagicMock()
4550
mock_torch.jit.load = MagicMock()
46-
51+
4752
mock_torchaudio = MagicMock()
4853
mock_torchaudio.functional.resample = MagicMock()
49-
50-
sys.modules['torch'] = mock_torch
51-
sys.modules['torchaudio'] = mock_torchaudio
52-
54+
55+
sys.modules["torch"] = mock_torch
56+
sys.modules["torchaudio"] = mock_torchaudio
57+
5358
# Mock faster_whisper
5459
try:
5560
import faster_whisper
@@ -58,8 +63,8 @@ def setup_ml_mocks():
5863
mock_whisper = MagicMock()
5964
mock_whisper.WhisperModel = MagicMock()
6065
mock_whisper.BatchedInferencePipeline = MagicMock()
61-
sys.modules['faster_whisper'] = mock_whisper
62-
66+
sys.modules["faster_whisper"] = mock_whisper
67+
6368
# Mock diart
6469
try:
6570
import diart
@@ -69,37 +74,37 @@ def setup_ml_mocks():
6974
mock_diart.SpeakerDiarization = MagicMock()
7075
mock_diart.SpeakerDiarizationConfig = MagicMock()
7176
mock_diart.models = MagicMock()
72-
sys.modules['diart'] = mock_diart
73-
sys.modules['diart.models'] = mock_diart.models
74-
77+
sys.modules["diart"] = mock_diart
78+
sys.modules["diart.models"] = mock_diart.models
79+
7580
# Mock pyannote
7681
try:
7782
import pyannote
7883
except ImportError:
7984
print("[pytest] Mocking pyannote")
8085
mock_pyannote = MagicMock()
8186
mock_pyannote.core.Annotation = MagicMock()
82-
sys.modules['pyannote'] = mock_pyannote
83-
sys.modules['pyannote.core'] = mock_pyannote.core
84-
87+
sys.modules["pyannote"] = mock_pyannote
88+
sys.modules["pyannote.core"] = mock_pyannote.core
89+
8590
# Mock openwakeword
8691
try:
8792
import openwakeword
8893
except ImportError:
8994
print("[pytest] Mocking openwakeword")
90-
sys.modules['openwakeword'] = MagicMock()
91-
95+
sys.modules["openwakeword"] = MagicMock()
96+
9297
# Mock silero_vad module that's causing the import error
9398
mock_silero = MagicMock()
9499
mock_silero.model.load_silero_vad = MagicMock()
95100
mock_silero.utils_vad.init_jit_model = MagicMock()
96101
mock_silero.utils_vad.OnnxWrapper = MagicMock()
97102
mock_silero.data = MagicMock() # Mock the missing data submodule
98-
sys.modules['silero_vad'] = mock_silero
99-
sys.modules['silero_vad.model'] = mock_silero.model
100-
sys.modules['silero_vad.utils_vad'] = mock_silero.utils_vad
101-
sys.modules['silero_vad.data'] = mock_silero.data
102-
103+
sys.modules["silero_vad"] = mock_silero
104+
sys.modules["silero_vad.model"] = mock_silero.model
105+
sys.modules["silero_vad.utils_vad"] = mock_silero.utils_vad
106+
sys.modules["silero_vad.data"] = mock_silero.data
107+
103108
print("[pytest] ML dependency mocking completed")
104109

105110

@@ -110,12 +115,24 @@ def setup_ml_mocks():
110115
# Basic pytest configuration
111116
def pytest_configure(config):
112117
"""Configure pytest for speech recognition tests"""
113-
config.addinivalue_line(
114-
"markers", "unit: Unit tests for speech components"
115-
)
116-
config.addinivalue_line(
117-
"markers", "integration: Integration tests with ROS"
118-
)
119-
config.addinivalue_line(
120-
"markers", "slow: Tests that require model loading"
121-
)
118+
config.addinivalue_line("markers", "unit: Unit tests for speech components")
119+
config.addinivalue_line("markers", "integration: Integration tests with ROS")
120+
config.addinivalue_line("markers", "slow: Tests that require model loading")
121+
122+
123+
# Memory optimization hooks
124+
@pytest.fixture(autouse=True)
125+
def cleanup_after_test():
126+
"""Force garbage collection after each test to prevent memory buildup"""
127+
yield
128+
gc.collect()
129+
130+
131+
def pytest_runtest_teardown(item, nextitem):
132+
"""Force garbage collection between tests to free memory"""
133+
gc.collect()
134+
135+
136+
def pytest_sessionfinish(session, exitstatus):
137+
"""Final cleanup after all tests complete"""
138+
gc.collect()

0 commit comments

Comments
 (0)