Merge branch 'main' into b2v

This commit is contained in:
r33drichards
2025-10-10 14:52:13 -07:00
committed by GitHub
7 changed files with 587 additions and 15 deletions

68
.github/scripts/get_pyproject_version.py vendored Executable file
View File

@@ -0,0 +1,68 @@
#!/usr/bin/env python3
"""
Verifies that the version in pyproject.toml matches the expected version.
Usage:
python get_pyproject_version.py <pyproject_path> <expected_version>
Exit codes:
0 - Versions match
1 - Versions don't match or error occurred
"""
import sys
try:
import tomllib
except ImportError:
# Fallback for Python < 3.11
import toml as tomllib
def main():
if len(sys.argv) != 3:
print("Usage: python get_pyproject_version.py <pyproject_path> <expected_version>", file=sys.stderr)
sys.exit(1)
pyproject_path = sys.argv[1]
expected_version = sys.argv[2]
# tomllib requires binary mode
try:
with open(pyproject_path, 'rb') as f:
data = tomllib.load(f)
except FileNotFoundError:
print(f"❌ ERROR: File not found: {pyproject_path}", file=sys.stderr)
sys.exit(1)
except Exception as e:
# Fallback to toml if using the old library or handle other errors
try:
import toml
data = toml.load(pyproject_path)
except FileNotFoundError:
print(f"❌ ERROR: File not found: {pyproject_path}", file=sys.stderr)
sys.exit(1)
except Exception as toml_err:
print(f"❌ ERROR: Failed to parse TOML file: {e}", file=sys.stderr)
sys.exit(1)
actual_version = data.get('project', {}).get('version')
if not actual_version:
print("❌ ERROR: No version found in pyproject.toml", file=sys.stderr)
sys.exit(1)
if actual_version != expected_version:
print("❌ Version mismatch detected!", file=sys.stderr)
print(f" pyproject.toml version: {actual_version}", file=sys.stderr)
print(f" Expected version: {expected_version}", file=sys.stderr)
print("", file=sys.stderr)
print("The version in pyproject.toml must match the version being published.", file=sys.stderr)
print(f"Please update pyproject.toml to version {expected_version} or use the correct tag.", file=sys.stderr)
sys.exit(1)
print(f"✅ Version consistency check passed: {actual_version}")
sys.exit(0)
if __name__ == '__main__':
main()

131
.github/scripts/tests/README.md vendored Normal file
View File

@@ -0,0 +1,131 @@
# Tests for .github/scripts
This directory contains comprehensive tests for the GitHub workflow scripts using Python's built-in testing framework.
## Requirements
**No external dependencies required!**
This test suite uses:
- `unittest` - Python's built-in testing framework
- `tomllib` - Python 3.11+ built-in TOML parser
For Python < 3.11, the `toml` package is used as a fallback.
## Running Tests
### Run all tests
```bash
cd .github/scripts/tests
python3 -m unittest discover -v
```
### Run a specific test file
```bash
python3 -m unittest test_get_pyproject_version -v
```
### Run a specific test class
```bash
python3 -m unittest test_get_pyproject_version.TestGetPyprojectVersion -v
```
### Run a specific test method
```bash
python3 -m unittest test_get_pyproject_version.TestGetPyprojectVersion.test_matching_versions -v
```
### Run tests directly from the test file
```bash
python3 test_get_pyproject_version.py
```
## Test Structure
### test_get_pyproject_version.py
Comprehensive tests for `get_pyproject_version.py` covering:
-**Version matching**: Tests successful version validation
-**Version mismatch**: Tests error handling when versions don't match
-**Missing version**: Tests handling of pyproject.toml without version field
-**Missing project section**: Tests handling of pyproject.toml without project section
-**File not found**: Tests handling of non-existent files
-**Malformed TOML**: Tests handling of invalid TOML syntax
-**Argument validation**: Tests proper argument count validation
-**Semantic versioning**: Tests various semantic version formats
-**Pre-release tags**: Tests versions with alpha, beta, rc tags
-**Build metadata**: Tests versions with build metadata
-**Edge cases**: Tests empty versions and other edge cases
**Total Tests**: 17+ test cases covering all functionality
## Best Practices Implemented
1. **Fixture Management**: Uses `setUp()` and `tearDown()` for clean test isolation
2. **Helper Methods**: Provides reusable helpers for creating test fixtures
3. **Temporary Files**: Uses `tempfile` for file creation with proper cleanup
4. **Comprehensive Coverage**: Tests happy paths, error conditions, and edge cases
5. **Clear Documentation**: Each test has a descriptive docstring
6. **Output Capture**: Uses `unittest.mock.patch` and `StringIO` to test stdout/stderr
7. **Exit Code Validation**: Properly tests script exit codes with `assertRaises(SystemExit)`
8. **Type Hints**: Uses type hints in helper methods for clarity
9. **PEP 8 Compliance**: Follows Python style guidelines
10. **Zero External Dependencies**: Uses only Python standard library
## Continuous Integration
These tests can be integrated into GitHub Actions workflows with no additional dependencies:
```yaml
- name: Run .github scripts tests
run: |
cd .github/scripts/tests
python3 -m unittest discover -v
```
## Test Output Example
```
test_empty_version_string (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of empty version string. ... ok
test_file_not_found (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of non-existent pyproject.toml file. ... ok
test_malformed_toml (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of malformed TOML file. ... ok
test_matching_versions (test_get_pyproject_version.TestGetPyprojectVersion)
Test that matching versions result in success. ... ok
test_missing_project_section (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of pyproject.toml without a project section. ... ok
test_missing_version_field (test_get_pyproject_version.TestGetPyprojectVersion)
Test handling of pyproject.toml without a version field. ... ok
test_no_arguments (test_get_pyproject_version.TestGetPyprojectVersion)
Test that providing no arguments results in usage error. ... ok
test_semantic_version_0_0_1 (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version 0.0.1. ... ok
test_semantic_version_1_0_0 (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version 1.0.0. ... ok
test_semantic_version_10_20_30 (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version 10.20.30. ... ok
test_semantic_version_alpha (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version with alpha tag. ... ok
test_semantic_version_beta (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version with beta tag. ... ok
test_semantic_version_rc_with_build (test_get_pyproject_version.TestGetPyprojectVersion)
Test semantic version with rc and build metadata. ... ok
test_too_few_arguments (test_get_pyproject_version.TestGetPyprojectVersion)
Test that providing too few arguments results in usage error. ... ok
test_too_many_arguments (test_get_pyproject_version.TestGetPyprojectVersion)
Test that providing too many arguments results in usage error. ... ok
test_version_mismatch (test_get_pyproject_version.TestGetPyprojectVersion)
Test that mismatched versions result in failure with appropriate error message. ... ok
test_version_with_build_metadata (test_get_pyproject_version.TestGetPyprojectVersion)
Test matching versions with build metadata. ... ok
test_version_with_prerelease_tags (test_get_pyproject_version.TestGetPyprojectVersion)
Test matching versions with pre-release tags like alpha, beta, rc. ... ok
----------------------------------------------------------------------
Ran 18 tests in 0.XXXs
OK
```

1
.github/scripts/tests/__init__.py vendored Normal file
View File

@@ -0,0 +1 @@
"""Tests for .github/scripts."""

View File

@@ -0,0 +1,340 @@
"""
Comprehensive tests for get_pyproject_version.py script using unittest.
This test suite covers:
- Version matching validation
- Error handling for missing versions
- Invalid input handling
- File not found scenarios
- Malformed TOML handling
"""
import sys
import unittest
import tempfile
from pathlib import Path
from io import StringIO
from unittest.mock import patch
# Add parent directory to path to import the module
sys.path.insert(0, str(Path(__file__).parent.parent))
# Import after path is modified
import get_pyproject_version
class TestGetPyprojectVersion(unittest.TestCase):
"""Test suite for get_pyproject_version.py functionality."""
def setUp(self):
"""Reset sys.argv before each test."""
self.original_argv = sys.argv.copy()
def tearDown(self):
"""Restore sys.argv after each test."""
sys.argv = self.original_argv
def create_pyproject_toml(self, version: str) -> Path:
"""Helper to create a temporary pyproject.toml file with a given version."""
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False)
temp_file.write(f"""
[project]
name = "test-project"
version = "{version}"
description = "A test project"
""")
temp_file.close()
return Path(temp_file.name)
def create_pyproject_toml_no_version(self) -> Path:
"""Helper to create a pyproject.toml without a version field."""
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False)
temp_file.write("""
[project]
name = "test-project"
description = "A test project without version"
""")
temp_file.close()
return Path(temp_file.name)
def create_pyproject_toml_no_project(self) -> Path:
"""Helper to create a pyproject.toml without a project section."""
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False)
temp_file.write("""
[tool.poetry]
name = "test-project"
version = "1.0.0"
""")
temp_file.close()
return Path(temp_file.name)
def create_malformed_toml(self) -> Path:
"""Helper to create a malformed TOML file."""
temp_file = tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False)
temp_file.write("""
[project
name = "test-project
version = "1.0.0"
""")
temp_file.close()
return Path(temp_file.name)
# Test: Successful version match
def test_matching_versions(self):
"""Test that matching versions result in success."""
pyproject_file = self.create_pyproject_toml("1.2.3")
try:
sys.argv = ['get_pyproject_version.py', str(pyproject_file), '1.2.3']
# Capture stdout
captured_output = StringIO()
with patch('sys.stdout', captured_output):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 0)
self.assertIn("✅ Version consistency check passed: 1.2.3", captured_output.getvalue())
finally:
pyproject_file.unlink()
# Test: Version mismatch
def test_version_mismatch(self):
"""Test that mismatched versions result in failure with appropriate error message."""
pyproject_file = self.create_pyproject_toml("1.2.3")
try:
sys.argv = ['get_pyproject_version.py', str(pyproject_file), '1.2.4']
# Capture stderr
captured_error = StringIO()
with patch('sys.stderr', captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
error_output = captured_error.getvalue()
self.assertIn("❌ Version mismatch detected!", error_output)
self.assertIn("pyproject.toml version: 1.2.3", error_output)
self.assertIn("Expected version: 1.2.4", error_output)
self.assertIn("Please update pyproject.toml to version 1.2.4", error_output)
finally:
pyproject_file.unlink()
# Test: Missing version in pyproject.toml
def test_missing_version_field(self):
"""Test handling of pyproject.toml without a version field."""
pyproject_file = self.create_pyproject_toml_no_version()
try:
sys.argv = ['get_pyproject_version.py', str(pyproject_file), '1.0.0']
captured_error = StringIO()
with patch('sys.stderr', captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn("❌ ERROR: No version found in pyproject.toml", captured_error.getvalue())
finally:
pyproject_file.unlink()
# Test: Missing project section
def test_missing_project_section(self):
"""Test handling of pyproject.toml without a project section."""
pyproject_file = self.create_pyproject_toml_no_project()
try:
sys.argv = ['get_pyproject_version.py', str(pyproject_file), '1.0.0']
captured_error = StringIO()
with patch('sys.stderr', captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn("❌ ERROR: No version found in pyproject.toml", captured_error.getvalue())
finally:
pyproject_file.unlink()
# Test: File not found
def test_file_not_found(self):
"""Test handling of non-existent pyproject.toml file."""
sys.argv = ['get_pyproject_version.py', '/nonexistent/pyproject.toml', '1.0.0']
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
# Test: Malformed TOML
def test_malformed_toml(self):
"""Test handling of malformed TOML file."""
pyproject_file = self.create_malformed_toml()
try:
sys.argv = ['get_pyproject_version.py', str(pyproject_file), '1.0.0']
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
finally:
pyproject_file.unlink()
# Test: Incorrect number of arguments - too few
def test_too_few_arguments(self):
"""Test that providing too few arguments results in usage error."""
sys.argv = ['get_pyproject_version.py', 'pyproject.toml']
captured_error = StringIO()
with patch('sys.stderr', captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn("Usage: python get_pyproject_version.py <pyproject_path> <expected_version>",
captured_error.getvalue())
# Test: Incorrect number of arguments - too many
def test_too_many_arguments(self):
"""Test that providing too many arguments results in usage error."""
sys.argv = ['get_pyproject_version.py', 'pyproject.toml', '1.0.0', 'extra']
captured_error = StringIO()
with patch('sys.stderr', captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn("Usage: python get_pyproject_version.py <pyproject_path> <expected_version>",
captured_error.getvalue())
# Test: No arguments
def test_no_arguments(self):
"""Test that providing no arguments results in usage error."""
sys.argv = ['get_pyproject_version.py']
captured_error = StringIO()
with patch('sys.stderr', captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
self.assertIn("Usage: python get_pyproject_version.py <pyproject_path> <expected_version>",
captured_error.getvalue())
# Test: Version with pre-release tags
def test_version_with_prerelease_tags(self):
"""Test matching versions with pre-release tags like alpha, beta, rc."""
pyproject_file = self.create_pyproject_toml("1.2.3-rc.1")
try:
sys.argv = ['get_pyproject_version.py', str(pyproject_file), '1.2.3-rc.1']
captured_output = StringIO()
with patch('sys.stdout', captured_output):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 0)
self.assertIn("✅ Version consistency check passed: 1.2.3-rc.1", captured_output.getvalue())
finally:
pyproject_file.unlink()
# Test: Version with build metadata
def test_version_with_build_metadata(self):
"""Test matching versions with build metadata."""
pyproject_file = self.create_pyproject_toml("1.2.3+build.123")
try:
sys.argv = ['get_pyproject_version.py', str(pyproject_file), '1.2.3+build.123']
captured_output = StringIO()
with patch('sys.stdout', captured_output):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 0)
self.assertIn("✅ Version consistency check passed: 1.2.3+build.123", captured_output.getvalue())
finally:
pyproject_file.unlink()
# Test: Various semantic version formats
def test_semantic_version_0_0_1(self):
"""Test semantic version 0.0.1."""
self._test_version_format("0.0.1")
def test_semantic_version_1_0_0(self):
"""Test semantic version 1.0.0."""
self._test_version_format("1.0.0")
def test_semantic_version_10_20_30(self):
"""Test semantic version 10.20.30."""
self._test_version_format("10.20.30")
def test_semantic_version_alpha(self):
"""Test semantic version with alpha tag."""
self._test_version_format("1.2.3-alpha")
def test_semantic_version_beta(self):
"""Test semantic version with beta tag."""
self._test_version_format("1.2.3-beta.1")
def test_semantic_version_rc_with_build(self):
"""Test semantic version with rc and build metadata."""
self._test_version_format("1.2.3-rc.1+build.456")
def _test_version_format(self, version: str):
"""Helper method to test various semantic version formats."""
pyproject_file = self.create_pyproject_toml(version)
try:
sys.argv = ['get_pyproject_version.py', str(pyproject_file), version]
captured_output = StringIO()
with patch('sys.stdout', captured_output):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 0)
self.assertIn(f"✅ Version consistency check passed: {version}", captured_output.getvalue())
finally:
pyproject_file.unlink()
# Test: Empty version string
def test_empty_version_string(self):
"""Test handling of empty version string."""
pyproject_file = self.create_pyproject_toml("")
try:
sys.argv = ['get_pyproject_version.py', str(pyproject_file), '1.0.0']
captured_error = StringIO()
with patch('sys.stderr', captured_error):
with self.assertRaises(SystemExit) as cm:
get_pyproject_version.main()
self.assertEqual(cm.exception.code, 1)
# Empty string is falsy, so it should trigger error
self.assertIn("", captured_error.getvalue())
finally:
pyproject_file.unlink()
class TestSuiteInfo(unittest.TestCase):
"""Test suite metadata."""
def test_suite_info(self):
"""Display test suite information."""
print("\n" + "="*70)
print("Test Suite: get_pyproject_version.py")
print("Framework: unittest (Python built-in)")
print("TOML Library: tomllib (Python 3.11+ built-in)")
print("="*70)
self.assertTrue(True)
if __name__ == '__main__':
# Run tests with verbose output
unittest.main(verbosity=2)

View File

@@ -71,6 +71,16 @@ jobs:
echo "VERSION=${{ inputs.version }}" >> $GITHUB_ENV
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
- name: Verify version consistency
run: |
# Install toml parser
pip install toml
# Verify version matches using script (exits with error if mismatch)
python ${GITHUB_WORKSPACE}/.github/scripts/get_pyproject_version.py \
${{ inputs.package_dir }}/pyproject.toml \
${{ inputs.version }}
- name: Initialize PDM in package directory
run: |
# Make sure we're working with a properly initialized PDM project
@@ -82,21 +92,6 @@ jobs:
pdm lock
fi
- name: Set version in package
run: |
cd ${{ inputs.package_dir }}
# Replace pdm bump with direct edit of pyproject.toml
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS version of sed needs an empty string for -i
sed -i '' "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml
else
# Linux version
sed -i "s/version = \".*\"/version = \"$VERSION\"/" pyproject.toml
fi
# Verify version was updated
echo "Updated version in pyproject.toml:"
grep "version =" pyproject.toml
# Conditional step for lume binary download (only for pylume package)
- name: Download and setup lume binary
if: inputs.is_lume_package

View File

@@ -0,0 +1,36 @@
name: Test valididation script
on:
pull_request:
paths:
- '.github/scripts/**'
- '.github/workflows/test-scripts.yml'
push:
branches:
- main
paths:
- '.github/scripts/**'
- '.github/workflows/test-scripts.yml'
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest toml
- name: Run tests
run: |
cd .github/scripts
pytest tests/ -v

View File

@@ -5,6 +5,7 @@ build-backend = "pdm.backend"
[project]
name = "cua-computer-server"
version = "0.1.26"
description = "Server component for the Computer-Use Interface (CUI) framework powering Cua"
authors = [
{ name = "TryCua", email = "gh@trycua.com" }