Phase 3: Auto-Lifecycle Management - Implementation Plan¶
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Enable zero-config usage - import opensysml; model = opensysml.load("file.sysml") works without manual server setup.
Architecture: Binary management module downloads sysml-grpc from GitHub releases, Connection auto-starts service with lockfile coordination, module-level API provides convenience functions.
Tech Stack: Python 3.8+, urllib/requests for downloads, subprocess for process management, fcntl/filelock for multi-process coordination, atexit for cleanup.
File Structure¶
New files:
- opensysml/binary.py - Binary download, platform detection, checksum verification
- tests/test_binary.py - Unit tests for binary management
- tests/test_lifecycle.py - Integration tests for auto-start
Modified files:
- opensysml/connection.py - Add auto-start logic, health checks, process management
- opensysml/__init__.py - Add module-level convenience API
- tests/test_connection.py - Add auto-start tests
Task Outline¶
Task 1: Binary Management Module¶
[Platform detection, GitHub release download, checksum verification, storage in ~/.opensysml/bin/]
Task 2: Service Auto-Start in Connection¶
[Health check probing, subprocess management, lockfile coordination, atexit cleanup]
Task 3: Module-Level Convenience API¶
[load(), connect(), auto-initialization on first use]
Task 4: Comprehensive Testing¶
[Mock HTTP downloads, service lifecycle, multi-process coordination]
Tasks (to be filled in)¶
Task 1: Binary Management Module¶
Files:
- Create: opensysml/binary.py
- Create: tests/test_binary.py
Objective: Detect platform, download sysml-grpc from GitHub releases, verify checksum, store in ~/.opensysml/bin/
- [ ] Step 1: Write failing test for platform detection
# tests/test_binary.py
import platform
from opensysml.binary import detect_platform
def test_detect_platform():
"""Test platform detection returns valid tuple."""
os_name, arch = detect_platform()
assert os_name in ('linux', 'darwin', 'windows')
assert arch in ('amd64', 'arm64')
- [ ] Step 2: Run test to verify it fails
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_binary.py::test_detect_platform -v
Expected: FAIL with "cannot import name 'detect_platform'"
- [ ] Step 3: Implement platform detection
# opensysml/binary.py
"""Binary management for sysml-grpc service."""
import platform
import sys
def detect_platform():
"""Detect current platform and architecture.
Returns:
tuple: (os_name, arch) where os_name in ('linux', 'darwin', 'windows')
and arch in ('amd64', 'arm64')
"""
# Map Python platform names to Go GOOS
system = platform.system().lower()
if system == 'linux':
os_name = 'linux'
elif system == 'darwin':
os_name = 'darwin'
elif system == 'windows':
os_name = 'windows'
else:
raise RuntimeError(f"Unsupported operating system: {system}")
# Map Python machine architecture to Go GOARCH
machine = platform.machine().lower()
if machine in ('x86_64', 'amd64'):
arch = 'amd64'
elif machine in ('aarch64', 'arm64'):
arch = 'arm64'
else:
raise RuntimeError(f"Unsupported architecture: {machine}")
return os_name, arch
- [ ] Step 4: Run test to verify it passes
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_binary.py::test_detect_platform -v
Expected: PASS
- [ ] Step 5: Write failing test for binary path
# tests/test_binary.py
import os
from opensysml.binary import get_binary_path
def test_get_binary_path():
"""Test binary path construction."""
path = get_binary_path()
assert path.startswith(os.path.expanduser('~/.opensysml/bin/'))
assert path.endswith('sysml-grpc') or path.endswith('sysml-grpc.exe')
- [ ] Step 6: Implement get_binary_path
# opensysml/binary.py (add to existing file)
import os
def get_binary_path():
"""Get the local path where sysml-grpc binary should be stored.
Returns:
str: Absolute path to binary (e.g. ~/.opensysml/bin/sysml-grpc)
"""
os_name, _ = detect_platform()
binary_name = 'sysml-grpc.exe' if os_name == 'windows' else 'sysml-grpc'
base_dir = os.path.expanduser('~/.opensysml/bin')
return os.path.join(base_dir, binary_name)
- [ ] Step 7: Run test to verify it passes
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_binary.py::test_get_binary_path -v
Expected: PASS
- [ ] Step 8: Write failing test for binary download
# tests/test_binary.py
from unittest.mock import patch, Mock, mock_open
from opensysml.binary import download_binary
def test_download_binary():
"""Test binary download from GitHub releases."""
with patch('urllib.request.urlopen') as mock_urlopen:
# Mock HTTP response
mock_response = Mock()
mock_response.read.return_value = b'fake binary content'
mock_response.__enter__.return_value = mock_response
mock_response.__exit__.return_value = False
mock_urlopen.return_value = mock_response
with patch('builtins.open', mock_open()) as mock_file:
with patch('os.makedirs'):
with patch('os.chmod'):
result = download_binary(version='v0.1.0')
assert result == os.path.expanduser('~/.opensysml/bin/sysml-grpc')
mock_urlopen.assert_called_once()
# Verify URL format
call_args = mock_urlopen.call_args[0][0]
assert 'github.com/Open-MBEE/OpenSysML/releases/download/v0.1.0' in call_args
- [ ] Step 9: Implement download_binary
# opensysml/binary.py (add to existing file)
import urllib.request
import os
import stat
def download_binary(version='latest', github_repo='Open-MBEE/OpenSysML'):
"""Download sysml-grpc binary from GitHub releases.
Args:
version (str): Release tag (e.g. 'v0.1.0') or 'latest'
github_repo (str): GitHub repository (org/repo)
Returns:
str: Path to downloaded binary
Raises:
RuntimeError: If download fails
"""
os_name, arch = detect_platform()
binary_path = get_binary_path()
# Construct GitHub release URL
# Format: https://github.com/Open-MBEE/OpenSysML/releases/download/v0.1.0/sysml-grpc-linux-amd64
if version == 'latest':
# For now, use a fixed version (latest API requires extra request)
version = 'v0.1.0'
binary_name = f"sysml-grpc-{os_name}-{arch}"
if os_name == 'windows':
binary_name += '.exe'
url = f"https://github.com/{github_repo}/releases/download/{version}/{binary_name}"
# Create directory if it doesn't exist
os.makedirs(os.path.dirname(binary_path), exist_ok=True)
# Download binary
try:
with urllib.request.urlopen(url) as response:
data = response.read()
# Write to file
with open(binary_path, 'wb') as f:
f.write(data)
# Make executable
os.chmod(binary_path, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
return binary_path
except Exception as e:
raise RuntimeError(f"Failed to download sysml-grpc binary: {e}")
- [ ] Step 10: Run test to verify it passes
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_binary.py::test_download_binary -v
Expected: PASS
- [ ] Step 11: Write failing test for checksum verification
# tests/test_binary.py
import hashlib
from opensysml.binary import verify_checksum
def test_verify_checksum():
"""Test SHA-256 checksum verification."""
data = b"test binary content"
expected = hashlib.sha256(data).hexdigest()
with patch('builtins.open', mock_open(read_data=data)):
assert verify_checksum('/fake/path', expected) == True
assert verify_checksum('/fake/path', 'wrong_hash') == False
- [ ] Step 12: Implement verify_checksum
# opensysml/binary.py (add to existing file)
import hashlib
def verify_checksum(binary_path, expected_sha256):
"""Verify SHA-256 checksum of binary file.
Args:
binary_path (str): Path to binary file
expected_sha256 (str): Expected SHA-256 hex digest
Returns:
bool: True if checksum matches, False otherwise
"""
sha256 = hashlib.sha256()
with open(binary_path, 'rb') as f:
while True:
chunk = f.read(8192)
if not chunk:
break
sha256.update(chunk)
actual = sha256.hexdigest()
return actual == expected_sha256
- [ ] Step 13: Run test to verify it passes
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_binary.py::test_verify_checksum -v
Expected: PASS
- [ ] Step 14: Write failing test for ensure_binary
# tests/test_binary.py
from opensysml.binary import ensure_binary
def test_ensure_binary_exists():
"""Test ensure_binary returns path when binary exists."""
with patch('os.path.exists', return_value=True):
with patch('os.access', return_value=True):
path = ensure_binary()
assert path == os.path.expanduser('~/.opensysml/bin/sysml-grpc')
def test_ensure_binary_downloads():
"""Test ensure_binary downloads if binary missing."""
with patch('os.path.exists', return_value=False):
with patch('opensysml.binary.download_binary') as mock_download:
mock_download.return_value = '/fake/path/sysml-grpc'
path = ensure_binary()
assert path == '/fake/path/sysml-grpc'
mock_download.assert_called_once()
- [ ] Step 15: Implement ensure_binary
# opensysml/binary.py (add to existing file)
def ensure_binary(force_download=False):
"""Ensure sysml-grpc binary is available, downloading if necessary.
Args:
force_download (bool): If True, download even if binary exists
Returns:
str: Path to binary
Raises:
RuntimeError: If binary cannot be obtained
"""
binary_path = get_binary_path()
# Check if binary already exists and is executable
if not force_download and os.path.exists(binary_path):
if os.access(binary_path, os.X_OK):
return binary_path
# Download binary
return download_binary()
- [ ] Step 16: Run tests to verify they pass
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_binary.py -v
Expected: All 5 tests PASS
- [ ] Step 17: Commit
git add opensysml/binary.py tests/test_binary.py
git commit -m "feat(python): implement binary management with platform detection and GitHub downloads"
Task 2: Service Auto-Start in Connection¶
Files:
- Modify: opensysml/connection.py
- Modify: tests/test_connection.py
Objective: Auto-start sysml-grpc service when Connection created, with health checks and multi-process coordination
- [ ] Step 1: Write failing test for service health check
# tests/test_connection.py (add to existing file)
from opensysml.connection import Connection
def test_probe_service_running():
"""Test health check detects running service."""
with patch('grpc.insecure_channel') as mock_channel:
mock_stub = Mock()
mock_stub.GetDiagnostics.return_value = Mock() # Success
with patch('opensysml.proto.sysml_pb2_grpc.SysMLServiceStub', return_value=mock_stub):
conn = Connection()
assert conn._probe_service('localhost', 50051) == True
def test_probe_service_not_running():
"""Test health check detects service not running."""
with patch('grpc.insecure_channel') as mock_channel:
mock_stub = Mock()
mock_stub.GetDiagnostics.side_effect = grpc.RpcError()
with patch('opensysml.proto.sysml_pb2_grpc.SysMLServiceStub', return_value=mock_stub):
conn = Connection()
assert conn._probe_service('localhost', 50051) == False
- [ ] Step 2: Run tests to verify they fail
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_connection.py::test_probe_service_running -v
Expected: FAIL with "AttributeError: 'Connection' object has no attribute '_probe_service'"
- [ ] Step 3: Implement _probe_service method
# opensysml/connection.py (add to Connection class)
import grpc
from opensysml.proto import sysml_pb2
def _probe_service(self, host, port, timeout=1.0):
"""Check if sysml-grpc service is running and responsive.
Args:
host (str): Service host
port (int): Service port
timeout (float): Timeout in seconds
Returns:
bool: True if service responds to health check
"""
try:
channel = grpc.insecure_channel(f"{host}:{port}")
stub = sysml_pb2_grpc.SysMLServiceStub(channel)
# Use GetDiagnostics with empty model hash as health check
request = sysml_pb2.DiagnosticsRequest(model_hash="")
stub.GetDiagnostics(request, timeout=timeout)
channel.close()
return True
except Exception:
return False
- [ ] Step 4: Run tests to verify they pass
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_connection.py::test_probe_service_running tests/test_connection.py::test_probe_service_not_running -v
Expected: Both tests PASS
- [ ] Step 5: Write failing test for service auto-start
# tests/test_connection.py
import subprocess
from opensysml.connection import Connection
def test_ensure_service_starts():
"""Test _ensure_service starts service if not running."""
with patch('opensysml.connection.Connection._probe_service', side_effect=[False, True]):
with patch('opensysml.binary.ensure_binary', return_value='/fake/sysml-grpc'):
with patch('subprocess.Popen') as mock_popen:
mock_process = Mock()
mock_process.pid = 12345
mock_popen.return_value = mock_process
conn = Connection()
conn._ensure_service()
mock_popen.assert_called_once()
assert conn._service_process is not None
def test_ensure_service_reuses_existing():
"""Test _ensure_service doesn't start if already running."""
with patch('opensysml.connection.Connection._probe_service', return_value=True):
with patch('subprocess.Popen') as mock_popen:
conn = Connection()
conn._ensure_service()
mock_popen.assert_not_called()
- [ ] Step 6: Implement _ensure_service method
# opensysml/connection.py (add to Connection class)
import subprocess
import time
import atexit
from opensysml.binary import ensure_binary
def __init__(self, host='localhost', port=50051, auto_start=True):
"""Initialize connection to sysml-grpc service.
Args:
host (str): Service host (default: localhost)
port (int): Service port (default: 50051)
auto_start (bool): Auto-start service if not running (default: True)
"""
self._host = host
self._port = port
self._auto_start = auto_start
self._service_process = None
# Auto-start if enabled
if auto_start:
self._ensure_service()
# Create gRPC channel
self._channel = grpc.insecure_channel(f"{host}:{port}")
self._stub = sysml_pb2_grpc.SysMLServiceStub(self._channel)
def _ensure_service(self, max_retries=5, retry_delay=0.5):
"""Ensure sysml-grpc service is running, starting if necessary.
Args:
max_retries (int): Max health check attempts after starting
retry_delay (float): Delay between health checks (seconds)
Raises:
RuntimeError: If service cannot be started
"""
# Check if service already running
if self._probe_service(self._host, self._port):
return
# Get binary path
binary_path = ensure_binary()
# Start service as subprocess
try:
self._service_process = subprocess.Popen(
[binary_path, '-port', str(self._port)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True # Detach from parent
)
# Register cleanup
atexit.register(self._cleanup_service)
# Wait for service to start
for attempt in range(max_retries):
time.sleep(retry_delay)
if self._probe_service(self._host, self._port):
return
raise RuntimeError(f"Service failed to start after {max_retries} attempts")
except Exception as e:
raise RuntimeError(f"Failed to start sysml-grpc service: {e}")
def _cleanup_service(self):
"""Cleanup service process on exit."""
if self._service_process:
try:
self._service_process.terminate()
self._service_process.wait(timeout=5)
except:
pass
- [ ] Step 7: Run tests to verify they pass
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_connection.py::test_ensure_service_starts tests/test_connection.py::test_ensure_service_reuses_existing -v
Expected: Both tests PASS
- [ ] Step 8: Update existing Connection tests
Modify existing tests to handle auto_start parameter:
# tests/test_connection.py (update existing tests)
def test_connection_init():
"""Test Connection initializes with default host/port."""
with patch('grpc.insecure_channel') as mock_channel:
with patch('opensysml.proto.sysml_pb2_grpc.SysMLServiceStub'):
conn = Connection(auto_start=False) # Add auto_start=False
assert conn._host == 'localhost'
assert conn._port == 50051
mock_channel.assert_called_once_with('localhost:50051')
Apply auto_start=False to all existing Connection() calls in tests to avoid triggering auto-start during unit tests.
- [ ] Step 9: Run all connection tests
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_connection.py -v
Expected: All tests PASS (including new auto-start tests)
- [ ] Step 10: Commit
git add opensysml/connection.py tests/test_connection.py
git commit -m "feat(python): add auto-start service capability to Connection"
Task 3: Module-Level Convenience API¶
Files:
- Modify: opensysml/__init__.py
- Create: tests/test_api.py
Objective: Provide simple opensysml.load() API with auto-initialization
- [ ] Step 1: Write failing test for module-level load()
# tests/test_api.py
import opensysml
from unittest.mock import patch, Mock
def test_opensysml_load():
"""Test opensysml.load() convenience function."""
with patch('opensysml.Connection') as mock_conn_class:
mock_conn = Mock()
mock_model = Mock()
mock_conn.load.return_value = mock_model
mock_conn_class.return_value = mock_conn
result = opensysml.load('test.sysml')
assert result == mock_model
mock_conn_class.assert_called_once_with(host='localhost', port=50051, auto_start=True)
mock_conn.load.assert_called_once_with('test.sysml')
- [ ] Step 2: Run test to verify it fails
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_api.py::test_opensysml_load -v
Expected: FAIL with "AttributeError: module 'opensysml' has no attribute 'load'"
- [ ] Step 3: Implement module-level load() function
# opensysml/__init__.py (modify existing file)
"""opensysml - Python bindings for OpenSysML SysML v2 implementation."""
__version__ = "0.1.0"
# Import core classes
from opensysml.connection import Connection
from opensysml.model import Model
from opensysml.symbol import Symbol
from opensysml.diagnostic import Diagnostic
# Module-level default connection
_default_connection = None
def _get_default_connection():
"""Get or create default Connection instance.
Returns:
Connection: Shared connection instance
"""
global _default_connection
if _default_connection is None:
_default_connection = Connection(auto_start=True)
return _default_connection
def load(file_path, host='localhost', port=50051):
"""Load a SysML model file.
Convenience function that uses a module-level Connection with auto-start.
Args:
file_path (str): Path to .sysml file
host (str): Service host (default: localhost)
port (int): Service port (default: 50051)
Returns:
Model: Parsed model
Example:
>>> import opensysml
>>> model = opensysml.load("spacecraft.sysml")
>>> print(model.root.name)
"""
conn = _get_default_connection()
return conn.load(file_path)
__all__ = ['Connection', 'Model', 'Symbol', 'Diagnostic', 'load', '__version__']
- [ ] Step 4: Run test to verify it passes
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_api.py::test_opensysml_load -v
Expected: PASS
- [ ] Step 5: Write failing test for connect() function
# tests/test_api.py
def test_opensysml_connect():
"""Test opensysml.connect() returns Connection."""
with patch('opensysml.Connection') as mock_conn_class:
mock_conn = Mock()
mock_conn_class.return_value = mock_conn
result = opensysml.connect(host='remote', port=9000)
assert result == mock_conn
mock_conn_class.assert_called_once_with(host='remote', port=9000, auto_start=True)
- [ ] Step 6: Implement connect() function
# opensysml/__init__.py (add to existing file)
def connect(host='localhost', port=50051, auto_start=True):
"""Create a new Connection to sysml-grpc service.
Args:
host (str): Service host (default: localhost)
port (int): Service port (default: 50051)
auto_start (bool): Auto-start service if not running (default: True)
Returns:
Connection: New connection instance
Example:
>>> import opensysml
>>> conn = opensysml.connect()
>>> model = conn.load("model.sysml")
"""
return Connection(host=host, port=port, auto_start=auto_start)
__all__ = ['Connection', 'Model', 'Symbol', 'Diagnostic', 'load', 'connect', '__version__']
- [ ] Step 7: Run test to verify it passes
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_api.py::test_opensysml_connect -v
Expected: PASS
- [ ] Step 8: Write test for default connection reuse
# tests/test_api.py
def test_default_connection_reuse():
"""Test load() reuses same connection instance."""
# Reset module state
opensysml._default_connection = None
with patch('opensysml.Connection') as mock_conn_class:
mock_conn = Mock()
mock_conn_class.return_value = mock_conn
opensysml.load('file1.sysml')
opensysml.load('file2.sysml')
# Connection created only once
assert mock_conn_class.call_count == 1
# load() called twice on same connection
assert mock_conn.load.call_count == 2
- [ ] Step 9: Run test to verify it passes
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_api.py::test_default_connection_reuse -v
Expected: PASS
- [ ] Step 10: Run all API tests
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_api.py -v
Expected: All 3 tests PASS
- [ ] Step 11: Commit
git add opensysml/__init__.py tests/test_api.py
git commit -m "feat(python): add module-level convenience API (load, connect)"
Task 4: Comprehensive Testing¶
Files:
- Create: tests/test_lifecycle.py
- Modify: tests/test_binary.py (add integration test)
Objective: Integration tests for full auto-lifecycle (binary download, service start, model load)
- [ ] Step 1: Write integration test for binary download and service start
# tests/test_lifecycle.py
"""Integration tests for auto-lifecycle management."""
import os
import pytest
import opensysml
from opensysml.binary import get_binary_path
@pytest.mark.integration
def test_binary_download_and_service_start():
"""Test binary downloads and service starts on first use.
This test requires internet access and GitHub releases to exist.
Skip if binary already present or network unavailable.
"""
binary_path = get_binary_path()
# Only run if binary doesn't already exist (clean env test)
if os.path.exists(binary_path):
pytest.skip("Binary already exists, cannot test download")
try:
# This should trigger: download binary → start service → load file
# Use a minimal SysML file for testing
test_file = "/tmp/test_phase3.sysml"
with open(test_file, 'w') as f:
f.write('package TestModel;')
model = opensysml.load(test_file)
assert model is not None
assert os.path.exists(binary_path)
# Cleanup
os.remove(test_file)
except RuntimeError as e:
if "Failed to download" in str(e):
pytest.skip(f"Network error or release unavailable: {e}")
raise
- [ ] Step 2: Write integration test for service persistence
# tests/test_lifecycle.py
@pytest.mark.integration
def test_service_persists_across_loads():
"""Test service stays running for multiple loads."""
# Create two test files
test_file1 = "/tmp/test1.sysml"
test_file2 = "/tmp/test2.sysml"
with open(test_file1, 'w') as f:
f.write('package Model1;')
with open(test_file2, 'w') as f:
f.write('package Model2;')
try:
# First load
model1 = opensysml.load(test_file1)
assert model1 is not None
# Second load (service should still be running)
model2 = opensysml.load(test_file2)
assert model2 is not None
# Both models should be different
assert model1.hash != model2.hash
finally:
os.remove(test_file1)
os.remove(test_file2)
- [ ] Step 3: Write integration test for custom host/port
# tests/test_lifecycle.py
@pytest.mark.integration
def test_custom_port():
"""Test service starts on custom port."""
import time
# Use non-standard port
test_port = 50052
test_file = "/tmp/test_custom_port.sysml"
with open(test_file, 'w') as f:
f.write('package CustomPortTest;')
try:
conn = opensysml.connect(port=test_port)
# Give service time to start
time.sleep(1)
model = conn.load(test_file)
assert model is not None
finally:
os.remove(test_file)
# Note: service cleanup handled by atexit
- [ ] Step 4: Write test for error handling (bad file)
# tests/test_lifecycle.py
def test_load_nonexistent_file():
"""Test load() handles missing files gracefully."""
with pytest.raises(Exception): # grpc.RpcError
opensysml.load("/nonexistent/file.sysml")
- [ ] Step 5: Add test for binary checksum verification
# tests/test_binary.py (add to existing file)
import tempfile
import hashlib
from opensysml.binary import verify_checksum, download_binary
def test_download_binary_checksum():
"""Test downloaded binary checksum verification (if available).
This test downloads a real binary and verifies its checksum.
Skip if no internet or release unavailable.
"""
pytest.mark.integration
try:
# Download to temp location
with tempfile.TemporaryDirectory() as tmpdir:
# Mock get_binary_path to use temp dir
with patch('opensysml.binary.get_binary_path', return_value=os.path.join(tmpdir, 'sysml-grpc')):
binary_path = download_binary(version='v0.1.0')
# Verify file exists
assert os.path.exists(binary_path)
# Verify it's executable
assert os.access(binary_path, os.X_OK)
# Compute checksum (for manual verification)
sha256 = hashlib.sha256()
with open(binary_path, 'rb') as f:
sha256.update(f.read())
print(f"Downloaded binary SHA-256: {sha256.hexdigest()}")
except RuntimeError as e:
if "Failed to download" in str(e):
pytest.skip(f"Network error or release unavailable: {e}")
raise
- [ ] Step 6: Run integration tests
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/test_lifecycle.py tests/test_binary.py::test_download_binary_checksum -v -m integration
Expected: Tests PASS or SKIP (if network unavailable)
Note: Integration tests may skip if: - Binary already downloaded - Network unavailable - GitHub release doesn't exist yet
- [ ] Step 7: Run full test suite
Run: PYTHONPATH=/home/han/IdeaProjects/OpenSysML pytest tests/ -v
Expected: All unit tests PASS, integration tests PASS or SKIP
- [ ] Step 8: Manual end-to-end verification
# In a fresh Python environment (or remove ~/.opensysml/bin/ first)
cd /home/han/IdeaProjects/OpenSysML
rm -rf ~/.opensysml # Clean slate
python3 -c "
import opensysml
model = opensysml.load('/home/han/Downloads/A1.sysml')
print(f'Loaded: {model.root.name}')
print(f'Diagnostics: {len(model.diagnostics)}')
"
Expected output:
And binary should exist:
- [ ] Step 9: Update README with zero-config usage
# opensysml/README.md (update "Usage" section)
## Usage
### Zero-Config (Phase 3)
The simplest way to use opensysml - just import and load:
```python
import opensysml
# First use downloads binary and starts service automatically
model = opensysml.load("spacecraft.sysml")
print(f"Model: {model.root.name}")
print(f"Diagnostics: {len(model.diagnostics)}")
# Navigate model
for child in model.root.children():
print(f" - {child.name} ({child.kind})")
# Find by name
part = model.find("SPACECRAFT_WET")
if part:
print(f"Found: {part.name}")
Manual Connection (Phase 2)¶
For more control over service lifecycle:
from opensysml import Connection
# Start service manually or connect to existing
with Connection() as conn:
model = conn.load("model.sysml")
# ... use model
Configuration¶
Binary downloads from: https://github.com/Open-MBEE/OpenSysML/releases
Binary stored in: ~/.opensysml/bin/sysml-grpc
To use a different service:
- [ ] **Step 10: Commit**
```bash
git add tests/test_lifecycle.py tests/test_binary.py opensysml/README.md
git commit -m "test(python): add integration tests for auto-lifecycle and update README"
Definition of Done¶
- [ ]
import opensysmlauto-downloads binary on first use - [ ]
model = opensysml.load("A1.sysml")works without manual service start - [ ] Multiple Python processes can import concurrently (lockfile prevents conflicts)
- [ ] Service shuts down when last process exits (reference counting)
- [ ] All unit tests pass:
pytest tests/test_binary.py - [ ] All integration tests pass:
pytest tests/test_lifecycle.py - [ ] Manual verification: Fresh Python env → import → load works