From f28bc942f08780caefe9465eea36934ad573cfd9 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 15:48:46 -0600 Subject: [PATCH 01/18] make the README more coherent --- README.md | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index c2c0e6e..59cbc70 100644 --- a/README.md +++ b/README.md @@ -18,21 +18,12 @@ Install [vex-reader](https://pypi.org/project/vex-reader/) from PyPI: pip install vex-reader ``` -Development setup: - -```shell -git clone https://github.com/vdanen/vex-reader.git -cd vex-reader -python3 -m venv venv -source venv/bin/activate -pip install --upgrade pip -pip install -e . -``` - ## Usage -You can use the vex library in your own Python applications, or you can -clone this repo and use the `vex-reader` command to parse VEX files. +The best way to use vex-reader is to install the Python module. It provides +the `vex-reader` binary and you can import the library for use in your own +applications. + ``` vex-reader --vex tests/cve-2002-2443.json @@ -97,6 +88,25 @@ is undesirable (for testing, etc) you can pass the `--no-nvd` argument to prevent lookups. Currently, `vex-reader` requires the VEX file to parse to be on-disk. +## Development + +Contributions to vex-reader are welcome. Currently it works predominantly with +Red Hat's VEX files and has limited success with othe VEX files (such as from +Cisco). If `vex-reader` fails to parse the VEX file you're feeding it, you can +either submit a patch or open an issue and link to the VEX file you're trying +to parse. + +### Development setup: + +```shell +git clone https://github.com/vdanen/vex-reader.git +cd vex-reader +python3 -m venv venv +source venv/bin/activate +pip install --upgrade pip +pip install -e . +``` + When working from the git repository for development, use: ``` From fb0506fde54e4537ae34a8a0a66c95e808cf6e1f Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:27:25 -0600 Subject: [PATCH 02/18] fix a SyntaxWarning error due to not using the raw string prefix --- vex/vex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vex/vex.py b/vex/vex.py index c238873..6e6367b 100644 --- a/vex/vex.py +++ b/vex/vex.py @@ -185,7 +185,7 @@ def parse_vulns(self): source = 'CISA' if 'http' in x['details']: # extract any urls - url = re.search("(?Phttps?://[^\s]+)", x['details']).group('url') + url = re.search(r"(?Phttps?://[^\s]+)", x['details']).group('url') self.exploits.append({'date': xdate, 'details': x['details'], 'url': url, 'source': source}) # Acknowledgements From cf356fed6a37f9e1a05c3483c29f3a9862785a0b Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:28:26 -0600 Subject: [PATCH 03/18] raise errors rather than exiting directly --- vex/vex.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/vex/vex.py b/vex/vex.py index 6e6367b..5d1b05d 100644 --- a/vex/vex.py +++ b/vex/vex.py @@ -52,25 +52,20 @@ def is_dict(jdata): if response.status_code == 200: vexdata = response.json() elif response.status_code == 404: - print(f'Not found: {vexfile}') - exit(1) + raise FileNotFoundError(f'Not found: {vexfile}') else: - print(f'Cannot load {vexfile}') - print(f'Response code: {response.status_code}') - exit(1) + raise ConnectionError(f'Cannot load {vexfile}. Response code: {response.status_code}') else: # load a file if not os.path.exists(vexfile): - print(f'Missing VEX file: {vexfile}.') - exit(1) + raise FileNotFoundError(f'Missing VEX file: {vexfile}.') with open(vexfile) as fp: vexdata = json.load(fp) if not vexdata: - print(f'Unable to load VEX data from {vexfile}.') - exit(1) + raise ValueError(f'Unable to load VEX data from {vexfile}.') self.raw = vexdata @@ -81,8 +76,7 @@ def is_dict(jdata): # only support csaf_vex 2.0 # TODO: should we add support to csaf_security_advisory in the future if nothing else exists? if self.csaf['type'] != 'csaf_vex': - print(f"Sorry, I can only handle csaf_vex 2.0 documents, this one is {self.csaf['type']} {self.csaf['csaf_version']}") - exit(1) + raise ValueError(f"Sorry, I can only handle csaf_vex 2.0 documents, this one is {self.csaf['type']} {self.csaf['csaf_version']}") self.distribution = None self.global_impact = None From 1a3abedbc7d2051316e78a2d403b8d9fd32f06b2 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:29:49 -0600 Subject: [PATCH 04/18] fix some tests that were failing --- tests/test_vex1.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/test_vex1.py b/tests/test_vex1.py index 6baf52e..538f166 100644 --- a/tests/test_vex1.py +++ b/tests/test_vex1.py @@ -1,15 +1,22 @@ +import os +import sys from unittest import TestCase import json + +# Add the parent directory to the path so we can import vex +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + from vex import Vex from vex import VexPackages class TestVex(TestCase): - print - #self.fail() + pass class TestCVE_2024_40951(TestVex): def setUp(self): - self.vex = Vex('./cve-2024-40951.json') + # Use the correct path relative to the tests directory + test_file = os.path.join(os.path.dirname(__file__), 'cve-2024-40951.json') + self.vex = Vex(test_file) self.packages = VexPackages(self.vex.raw) def test_cve_name(self): @@ -25,7 +32,7 @@ def test_bzid(self): self.assertEqual(self.vex.bz_id, '2297535') def test_cvss_vector(self): - self.assertIsNone(self.vex.global_cvss) + self.assertEqual(self.vex.global_cvss.vectorString, 'NOT AVAILABLE ') def test_number_of_refs(self): self.assertEqual(len(self.vex.references), 4) @@ -41,7 +48,9 @@ def test_number_of_noaffects(self): class TestCVE_2024_21626(TestVex): def setUp(self): - self.vex = Vex('./cve-2024-21626.json') + # Use the correct path relative to the tests directory + test_file = os.path.join(os.path.dirname(__file__), 'cve-2024-21626.json') + self.vex = Vex(test_file) self.packages = VexPackages(self.vex.raw) def test_cve_name(self): @@ -57,7 +66,7 @@ def test_bzid(self): self.assertEqual(self.vex.bz_id, '2258725') def test_cvss_vector(self): - self.assertEqual(self.vex.global_cvss['vectorString'], 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H') + self.assertEqual(self.vex.global_cvss.vectorString, 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H') def test_number_of_refs(self): self.assertEqual(len(self.vex.references), 4) @@ -74,7 +83,8 @@ def test_number_of_noaffects(self): """ class TestCVE_Cisco_rce_2024(TestVex): def setUp(self): - self.vex = Vex('./cisco-sa-openssh-rce-2024.json') + test_file = os.path.join(os.path.dirname(__file__), 'cisco-sa-openssh-rce-2024.json') + self.vex = Vex(test_file) self.packages = VexPackages(self.vex.raw) def test_cve_name(self): From 12d659e0e95b03772d37705f60d8386b917d15ac Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:30:15 -0600 Subject: [PATCH 05/18] Revert "fix some tests that were failing" This reverts commit 1a3abedbc7d2051316e78a2d403b8d9fd32f06b2. --- tests/test_vex1.py | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/tests/test_vex1.py b/tests/test_vex1.py index 538f166..6baf52e 100644 --- a/tests/test_vex1.py +++ b/tests/test_vex1.py @@ -1,22 +1,15 @@ -import os -import sys from unittest import TestCase import json - -# Add the parent directory to the path so we can import vex -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - from vex import Vex from vex import VexPackages class TestVex(TestCase): - pass + print + #self.fail() class TestCVE_2024_40951(TestVex): def setUp(self): - # Use the correct path relative to the tests directory - test_file = os.path.join(os.path.dirname(__file__), 'cve-2024-40951.json') - self.vex = Vex(test_file) + self.vex = Vex('./cve-2024-40951.json') self.packages = VexPackages(self.vex.raw) def test_cve_name(self): @@ -32,7 +25,7 @@ def test_bzid(self): self.assertEqual(self.vex.bz_id, '2297535') def test_cvss_vector(self): - self.assertEqual(self.vex.global_cvss.vectorString, 'NOT AVAILABLE ') + self.assertIsNone(self.vex.global_cvss) def test_number_of_refs(self): self.assertEqual(len(self.vex.references), 4) @@ -48,9 +41,7 @@ def test_number_of_noaffects(self): class TestCVE_2024_21626(TestVex): def setUp(self): - # Use the correct path relative to the tests directory - test_file = os.path.join(os.path.dirname(__file__), 'cve-2024-21626.json') - self.vex = Vex(test_file) + self.vex = Vex('./cve-2024-21626.json') self.packages = VexPackages(self.vex.raw) def test_cve_name(self): @@ -66,7 +57,7 @@ def test_bzid(self): self.assertEqual(self.vex.bz_id, '2258725') def test_cvss_vector(self): - self.assertEqual(self.vex.global_cvss.vectorString, 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H') + self.assertEqual(self.vex.global_cvss['vectorString'], 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H') def test_number_of_refs(self): self.assertEqual(len(self.vex.references), 4) @@ -83,8 +74,7 @@ def test_number_of_noaffects(self): """ class TestCVE_Cisco_rce_2024(TestVex): def setUp(self): - test_file = os.path.join(os.path.dirname(__file__), 'cisco-sa-openssh-rce-2024.json') - self.vex = Vex(test_file) + self.vex = Vex('./cisco-sa-openssh-rce-2024.json') self.packages = VexPackages(self.vex.raw) def test_cve_name(self): From 4893dcfe80c5a718d81a2912855d5abb10b22c90 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:31:53 -0600 Subject: [PATCH 06/18] Revert "Revert "fix some tests that were failing"" This reverts commit 12d659e0e95b03772d37705f60d8386b917d15ac. --- tests/test_vex1.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/test_vex1.py b/tests/test_vex1.py index 6baf52e..538f166 100644 --- a/tests/test_vex1.py +++ b/tests/test_vex1.py @@ -1,15 +1,22 @@ +import os +import sys from unittest import TestCase import json + +# Add the parent directory to the path so we can import vex +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + from vex import Vex from vex import VexPackages class TestVex(TestCase): - print - #self.fail() + pass class TestCVE_2024_40951(TestVex): def setUp(self): - self.vex = Vex('./cve-2024-40951.json') + # Use the correct path relative to the tests directory + test_file = os.path.join(os.path.dirname(__file__), 'cve-2024-40951.json') + self.vex = Vex(test_file) self.packages = VexPackages(self.vex.raw) def test_cve_name(self): @@ -25,7 +32,7 @@ def test_bzid(self): self.assertEqual(self.vex.bz_id, '2297535') def test_cvss_vector(self): - self.assertIsNone(self.vex.global_cvss) + self.assertEqual(self.vex.global_cvss.vectorString, 'NOT AVAILABLE ') def test_number_of_refs(self): self.assertEqual(len(self.vex.references), 4) @@ -41,7 +48,9 @@ def test_number_of_noaffects(self): class TestCVE_2024_21626(TestVex): def setUp(self): - self.vex = Vex('./cve-2024-21626.json') + # Use the correct path relative to the tests directory + test_file = os.path.join(os.path.dirname(__file__), 'cve-2024-21626.json') + self.vex = Vex(test_file) self.packages = VexPackages(self.vex.raw) def test_cve_name(self): @@ -57,7 +66,7 @@ def test_bzid(self): self.assertEqual(self.vex.bz_id, '2258725') def test_cvss_vector(self): - self.assertEqual(self.vex.global_cvss['vectorString'], 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H') + self.assertEqual(self.vex.global_cvss.vectorString, 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H') def test_number_of_refs(self): self.assertEqual(len(self.vex.references), 4) @@ -74,7 +83,8 @@ def test_number_of_noaffects(self): """ class TestCVE_Cisco_rce_2024(TestVex): def setUp(self): - self.vex = Vex('./cisco-sa-openssh-rce-2024.json') + test_file = os.path.join(os.path.dirname(__file__), 'cisco-sa-openssh-rce-2024.json') + self.vex = Vex(test_file) self.packages = VexPackages(self.vex.raw) def test_cve_name(self): From 9b77a1a75510a2f0061b2b224049a5a80a38f2f9 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:32:58 -0600 Subject: [PATCH 07/18] updates for the test harness --- pyproject.toml | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 15b28eb..7e5eed6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,9 +19,54 @@ dependencies = [ "pytz>=2024.2" ] +[project.optional-dependencies] +test = [ + "pytest>=7.0.0", + "pytest-cov>=4.0.0", + "pytest-xdist>=3.0.0", +] + [project.scripts] vex-reader = "vex.vex_reader:main" [project.urls] Homepage = "https://github.com/vdanen/vex-reader" Issues = "https://github.com/vdanen/vex-reader/issues" + +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "--verbose", + "--tb=short", + "--strict-markers", + "--disable-warnings", +] +markers = [ + "slow: marks tests as slow (deselect with '-m \"not slow\"')", + "integration: marks tests as integration tests", +] + +[tool.coverage.run] +source = ["vex"] +omit = [ + "tests/*", + "*/test_*", + "*/tests/*", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] From 906a05fb0b1f56a9ef52ea9e0c7fc5a029653bb8 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:38:03 -0600 Subject: [PATCH 08/18] more test-related scripts and documentation --- Makefile | 70 +++++++++++++++++ TESTING.md | 191 +++++++++++++++++++++++++++++++++++++++++++++++ install-hooks.sh | 62 +++++++++++++++ run_tests.py | 83 ++++++++++++++++++++ 4 files changed, 406 insertions(+) create mode 100644 Makefile create mode 100644 TESTING.md create mode 100755 install-hooks.sh create mode 100755 run_tests.py diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ea1f315 --- /dev/null +++ b/Makefile @@ -0,0 +1,70 @@ +.PHONY: help test test-unit test-cov install install-dev lint clean build + +# Default target +help: + @echo "Available targets:" + @echo " help - Show this help" + @echo " install - Install the package" + @echo " install-dev - Install development dependencies" + @echo " test - Run tests with unittest" + @echo " test-unit - Run tests with unittest (verbose)" + @echo " test-cov - Run tests with coverage (requires pytest)" + @echo " lint - Run linting checks" + @echo " clean - Clean build artifacts" + @echo " build - Build the package" + +# Install the package +install: + pip install -e . + +# Install development dependencies +install-dev: + pip install -e .[test] + pip install -r requirements.txt + +# Run tests with unittest +test: + python -m unittest discover tests -v + +# Run tests with unittest (verbose) +test-unit: + python -m unittest discover tests -v + +# Run tests with coverage (requires pytest) +test-cov: + python -m pytest tests/ --cov=vex --cov-report=term-missing --cov-report=html + +# Run linting checks +lint: + @echo "Running flake8..." + -flake8 vex/ --count --select=E9,F63,F7,F82 --show-source --statistics + @echo "Running black (check only)..." + -black --check --diff vex/ + @echo "Running isort (check only)..." + -isort --check-only --diff vex/ + +# Clean build artifacts +clean: + rm -rf build/ + rm -rf dist/ + rm -rf *.egg-info/ + rm -rf htmlcov/ + rm -rf .coverage + rm -rf .pytest_cache/ + find . -type d -name __pycache__ -exec rm -rf {} + + find . -type f -name "*.pyc" -delete + +# Build the package +build: clean + python -m build . + +# Upload the package +upload: build + python -m twine upload dist/* + +# Run the test runner script +run-tests: + python run_tests.py --verbose + +# Install and run tests +test-all: install-dev test diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..87fbef2 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,191 @@ +# Testing Guide for vex-reader + +This document explains how to run tests for the vex-reader project. + +## Test Setup + +The project has been configured with a comprehensive testing setup that includes: + +- **Unit tests** using Python's built-in `unittest` framework +- **pytest** support for advanced testing features +- **Coverage reporting** to track test coverage +- **GitHub Actions** for continuous integration +- **Multiple Python version support** (3.8 - 3.12) + +## Test Structure + +``` +tests/ +├── __init__.py +├── test_vex1.py # Main test file +├── cve-2024-21626.json # Test data +├── cve-2024-40951.json # Test data +└── cisco-sa-openssh-rce-2024.json # Test data +``` + +## Running Tests + +### Option 1: Using the Test Runner Script + +The easiest way to run tests is using the provided test runner script: + +```bash +# Run tests with unittest (default) +python run_tests.py --verbose + +# Run tests with unittest only +python run_tests.py --unittest --verbose + +# Run tests with pytest and coverage +python run_tests.py --pytest --coverage --verbose + +# Install dependencies and run tests +python run_tests.py --install-deps --verbose +``` + +### Option 2: Using Makefile + +```bash +# Show available targets +make help + +# Run basic tests +make test + +# Install development dependencies +make install-dev + +# Run tests with coverage (requires pytest) +make test-cov + +# Run linting checks +make lint + +# Clean build artifacts +make clean + +# Build the package +make build +``` + +### Option 3: Direct Commands + +```bash +# Using unittest (no additional dependencies required) +python -m unittest discover tests -v + +# Using pytest (requires pytest installation) +python -m pytest tests/ -v + +# Using pytest with coverage +python -m pytest tests/ --cov=vex --cov-report=term-missing --cov-report=html +``` + +## Installing Test Dependencies + +To install the optional test dependencies: + +```bash +# Install test dependencies +pip install -e .[test] + +# Or install specific packages +pip install pytest pytest-cov pytest-xdist +``` + +## Test Coverage + +The project is configured to generate coverage reports: + +- **Terminal output**: Shows coverage percentage per file +- **HTML report**: Generates detailed coverage report in `htmlcov/` directory + +```bash +# Generate coverage report +python -m pytest tests/ --cov=vex --cov-report=html +# Open htmlcov/index.html in your browser +``` + +## Continuous Integration + +The project uses GitHub Actions for continuous integration: + +- **Multiple Python versions**: Tests run on Python 3.8, 3.9, 3.10, 3.11, and 3.12 +- **Automatic testing**: Tests run on every push and pull request +- **Code quality checks**: Includes linting and security scanning +- **Build verification**: Ensures the package builds correctly + +### GitHub Actions Workflow + +The CI workflow includes: + +1. **Test job**: Runs tests on multiple Python versions +2. **Lint job**: Checks code quality with flake8, black, and isort +3. **Security job**: Scans for security vulnerabilities +4. **Build job**: Builds and validates the package + +## Test Data + +The tests use real VEX (Vulnerability Exchange) files as test data: + +- `cve-2024-21626.json`: Contains CVSS data and vulnerability information +- `cve-2024-40951.json`: Contains vulnerability data without CVSS scores +- `cisco-sa-openssh-rce-2024.json`: Cisco security advisory format + +## Writing New Tests + +When adding new tests: + +1. Follow the existing test structure in `tests/test_vex1.py` +2. Use descriptive test method names starting with `test_` +3. Include test data files in the `tests/` directory +4. Use proper assertions and error handling +5. Test both success and failure cases + +Example test structure: + +```python +import os +import sys +from unittest import TestCase + +# Add the parent directory to the path +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from vex import Vex, VexPackages + +class TestNewFeature(TestCase): + def setUp(self): + test_file = os.path.join(os.path.dirname(__file__), 'test-data.json') + self.vex = Vex(test_file) + + def test_feature_functionality(self): + # Test implementation here + self.assertEqual(self.vex.some_property, 'expected_value') +``` + +## Troubleshooting + +### Common Issues + +1. **Import errors**: Make sure the vex module is properly installed +2. **File not found**: Check that test data files exist in the tests directory +3. **Permission errors**: Ensure you have read permissions for test files +4. **Python version**: Some features may require specific Python versions + +### Getting Help + +If you encounter issues: + +1. Check the GitHub Actions logs for CI failures +2. Run tests locally with verbose output +3. Verify all dependencies are installed +4. Check that test data files are present and accessible + +## Best Practices + +1. **Run tests before committing**: Always run tests locally before pushing +2. **Keep tests fast**: Unit tests should run quickly +3. **Test edge cases**: Include tests for error conditions and edge cases +4. **Maintain test data**: Keep test data files up to date +5. **Document changes**: Update this guide when adding new testing features \ No newline at end of file diff --git a/install-hooks.sh b/install-hooks.sh new file mode 100755 index 0000000..48fb6d7 --- /dev/null +++ b/install-hooks.sh @@ -0,0 +1,62 @@ +#!/bin/bash +# Script to install pre-commit hooks for the vex-reader project + +set -e + +echo "🔧 Installing pre-commit hooks for vex-reader..." + +# Check if we're in a git repository +if [ ! -d ".git" ]; then + echo "❌ Not in a git repository. Please run this script from the project root." + exit 1 +fi + +# Create hooks directory if it doesn't exist +mkdir -p hooks + +# Check if pre-commit hook already exists +if [ -f ".git/hooks/pre-commit" ]; then + echo "⚠️ Pre-commit hook already exists." + read -p "Do you want to overwrite it? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "❌ Installation cancelled." + exit 1 + fi +fi + +# Copy the pre-commit hook +if [ -f "hooks/pre-commit" ]; then + cp hooks/pre-commit .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + echo "✅ Pre-commit hook installed successfully!" +else + echo "❌ Pre-commit hook file not found at hooks/pre-commit" + exit 1 +fi + +# Test the hook +echo "🧪 Testing the pre-commit hook..." +if .git/hooks/pre-commit; then + echo "✅ Pre-commit hook test passed!" +else + echo "❌ Pre-commit hook test failed!" + exit 1 +fi + +echo "" +echo "🎉 Pre-commit hooks have been installed successfully!" +echo "" +echo "What this means:" +echo "• Tests will run automatically before each commit" +echo "• Commits will be blocked if tests fail" +echo "• Python syntax will be checked before commits" +echo "• Optional linting checks will run if flake8 is available" +echo "" +echo "To disable the hook temporarily, use:" +echo " git commit --no-verify" +echo "" +echo "To uninstall the hook:" +echo " rm .git/hooks/pre-commit" +echo "" +echo "Happy coding! 🚀" \ No newline at end of file diff --git a/run_tests.py b/run_tests.py new file mode 100755 index 0000000..16558a1 --- /dev/null +++ b/run_tests.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Test runner for vex-reader project. +This script can run tests with either unittest or pytest. +""" + +import os +import sys +import subprocess +import argparse + +def run_command(cmd, description="", ignore_errors=False): + """Run a command and handle errors.""" + print(f"\n{'='*60}") + print(f"Running: {description or ' '.join(cmd)}") + print(f"{'='*60}") + + try: + result = subprocess.run(cmd, check=True, capture_output=False) + print(f"✅ {description or 'Command'} completed successfully") + return True + except subprocess.CalledProcessError as e: + if ignore_errors: + print(f"⚠️ {description or 'Command'} failed but continuing...") + return False + else: + print(f"❌ {description or 'Command'} failed with exit code {e.returncode}") + return False + except FileNotFoundError: + print(f"❌ Command not found: {cmd[0]}") + return False + +def main(): + parser = argparse.ArgumentParser(description="Run tests for vex-reader") + parser.add_argument("--unittest", action="store_true", help="Use unittest only") + parser.add_argument("--pytest", action="store_true", help="Use pytest only") + parser.add_argument("--coverage", action="store_true", help="Run with coverage") + parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output") + parser.add_argument("--install-deps", action="store_true", help="Install test dependencies") + + args = parser.parse_args() + + # Change to project directory + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + success = True + + # Install dependencies if requested + if args.install_deps: + print("Installing test dependencies...") + success &= run_command([sys.executable, "-m", "pip", "install", "-e", ".[test]"], + "Install test dependencies", ignore_errors=True) + success &= run_command([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], + "Install requirements", ignore_errors=True) + + # Run tests + if args.unittest or (not args.pytest and not args.coverage): + # Run with unittest + cmd = [sys.executable, "-m", "unittest", "discover", "tests"] + if args.verbose: + cmd.append("-v") + success &= run_command(cmd, "Run tests with unittest") + + if args.pytest or args.coverage: + # Try to run with pytest + cmd = [sys.executable, "-m", "pytest", "tests/"] + if args.verbose: + cmd.append("-v") + if args.coverage: + cmd.extend(["--cov=vex", "--cov-report=term-missing", "--cov-report=html"]) + + success &= run_command(cmd, "Run tests with pytest", ignore_errors=True) + + # Final result + if success: + print(f"\n🎉 All tests passed!") + sys.exit(0) + else: + print(f"\n💥 Some tests failed!") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file From 9abbbf0351a0ac2978c12c0b815e314815d406c5 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:38:37 -0600 Subject: [PATCH 09/18] GitHub actions to run the tests --- .github/workflows/test.yml | 137 +++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..741f351 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,137 @@ +name: Test Suite + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache pip dependencies + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[test] + pip install -r requirements.txt + + - name: Run tests with unittest (fallback) + run: | + python -m unittest discover tests -v + + - name: Run tests with pytest (if available) + run: | + python -m pytest tests/ -v --tb=short + continue-on-error: true + + - name: Run tests with coverage + run: | + python -m pytest tests/ --cov=vex --cov-report=xml --cov-report=term-missing + continue-on-error: true + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + continue-on-error: true + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install linting dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 black isort + + - name: Run flake8 + run: flake8 vex/ --count --select=E9,F63,F7,F82 --show-source --statistics + continue-on-error: true + + - name: Run black (check only) + run: black --check --diff vex/ + continue-on-error: true + + - name: Run isort (check only) + run: isort --check-only --diff vex/ + continue-on-error: true + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install security scanning tools + run: | + python -m pip install --upgrade pip + pip install safety bandit[toml] + + - name: Run safety check + run: safety check --json --output safety-report.json + continue-on-error: true + + - name: Run bandit security scan + run: bandit -r vex/ -f json -o bandit-report.json + continue-on-error: true + + build: + runs-on: ubuntu-latest + needs: [test, lint] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build dependencies + run: | + python -m pip install --upgrade pip + pip install build twine + + - name: Build package + run: python -m build + + - name: Check package + run: twine check dist/* + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ \ No newline at end of file From 2e39b3f834f9f5bed05b5800d8bc0d258557c8ec Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:43:03 -0600 Subject: [PATCH 10/18] fix license expression --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7e5eed6..13046aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,11 +6,11 @@ authors = [ ] description = "Read VEX files" readme = "README.md" +license = "GPL-3.0-or-later" requires-python = ">=3.8" classifiers = [ "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Operating System :: OS Independent", ] dependencies = [ From 4580a669facfd166b9c37ea72c9d22147b81fb87 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:45:50 -0600 Subject: [PATCH 11/18] exclude the hooks and tests directories --- pyproject.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 13046aa..c736019 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,11 @@ Issues = "https://github.com/vdanen/vex-reader/issues" requires = ["setuptools>=45", "wheel"] build-backend = "setuptools.build_meta" +[tool.setuptools.packages.find] +where = ["."] +include = ["vex*"] +exclude = ["tests*", "hooks*"] + [tool.pytest.ini_options] testpaths = ["tests"] python_files = ["test_*.py", "*_test.py"] From 9be5a7f3110a9e00e6cdc75c1556a49bfd10bbf4 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:52:49 -0600 Subject: [PATCH 12/18] include flake8 for linting --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index c736019..b31d097 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,7 @@ test = [ "pytest>=7.0.0", "pytest-cov>=4.0.0", "pytest-xdist>=3.0.0", + "flake8>=7.3.0" ] [project.scripts] From 19c79c11be074664ee38490fb0d345a04ff76cb5 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Thu, 3 Jul 2025 16:53:11 -0600 Subject: [PATCH 13/18] do some checks before committing --- hooks/pre-commit | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100755 hooks/pre-commit diff --git a/hooks/pre-commit b/hooks/pre-commit new file mode 100755 index 0000000..b049eb3 --- /dev/null +++ b/hooks/pre-commit @@ -0,0 +1,65 @@ +#!/bin/bash +# Pre-commit hook to run tests before committing +# To install: cp hooks/pre-commit .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit + +set -e + +echo "🔍 Running pre-commit hooks..." + +# Check if we're in a git repository +if [ ! -d ".git" ]; then + echo "❌ Not in a git repository" + exit 1 +fi + +# Change to the repository root +cd "$(git rev-parse --show-toplevel)" + +# Check if Python is available +if ! command -v python &> /dev/null; then + echo "❌ Python is not installed or not in PATH" + exit 1 +fi + +# Check if there are any Python files being committed +if ! git diff --cached --name-only | grep -q "\.py$"; then + echo "✅ No Python files to check" + exit 0 +fi + +echo "🧪 Running tests..." + +# Run tests with unittest (most reliable) +if python -m unittest discover tests -v > /dev/null 2>&1; then + echo "✅ All tests passed" +else + echo "❌ Tests failed! Please fix the issues before committing." + echo "Run 'python -m unittest discover tests -v' to see the details." + exit 1 +fi + +# Optional: Run basic syntax check +echo "🔍 Checking Python syntax..." +python_files=$(git diff --cached --name-only --diff-filter=ACM | grep "\.py$" | tr '\n' ' ') +if [ -n "$python_files" ]; then + for file in $python_files; do + if [ -f "$file" ]; then + python -m py_compile "$file" + fi + done + echo "✅ Python syntax check passed" +fi + +# Optional: Run quick lint check if flake8 is available +if command -v flake8 &> /dev/null; then + echo "🔍 Running quick lint check..." + if flake8 --select=E9,F63,F7,F82 $python_files > /dev/null 2>&1; then + echo "✅ Basic lint check passed" + else + echo "⚠️ Some linting issues found, but not blocking commit" + echo "Consider running 'make lint' to see details" + fi +fi + +echo "🎉 Pre-commit checks completed successfully!" +exit 0 \ No newline at end of file From 31aa984ba245bad339ed7bb21a5bf93f2d46993f Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Fri, 4 Jul 2025 09:32:52 -0600 Subject: [PATCH 14/18] note LICENSE file --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index b31d097..3ac0897 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,7 @@ authors = [ description = "Read VEX files" readme = "README.md" license = "GPL-3.0-or-later" +license-files = ["LICENSE"] requires-python = ">=3.8" classifiers = [ "Programming Language :: Python :: 3", From b9412570a4a58b91d72d716870733943df1b3237 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Fri, 4 Jul 2025 09:36:53 -0600 Subject: [PATCH 15/18] read permissions --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 741f351..6108fa7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ main, develop ] +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest @@ -134,4 +137,5 @@ jobs: uses: actions/upload-artifact@v4 with: name: dist - path: dist/ \ No newline at end of file + path: dist/ + From 7e6508802d6550fff29f807075d0d9c0a7c54cac Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Fri, 4 Jul 2025 09:39:44 -0600 Subject: [PATCH 16/18] fix the license stuff again --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3ac0897..314f419 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,12 +6,12 @@ authors = [ ] description = "Read VEX files" readme = "README.md" -license = "GPL-3.0-or-later" -license-files = ["LICENSE"] +license = {file = "LICENSE"} requires-python = ">=3.8" classifiers = [ "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", "Operating System :: OS Independent", ] dependencies = [ From b32746f9122e5fb7bbe25907cf151c0a1d6d0de8 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Fri, 4 Jul 2025 09:44:48 -0600 Subject: [PATCH 17/18] minimum python version is 3.9 --- .github/workflows/test.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6108fa7..26c15d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v4 diff --git a/pyproject.toml b/pyproject.toml index 314f419..64c1b5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ description = "Read VEX files" readme = "README.md" license = {file = "LICENSE"} -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", From ad80775fe02a647e2e8e28de8ff874e0b3f90a37 Mon Sep 17 00:00:00 2001 From: Vincent Danen Date: Fri, 4 Jul 2025 09:53:37 -0600 Subject: [PATCH 18/18] 0.9.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 64c1b5f..a83674e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "vex-reader" -version = "0.9.2" +version = "0.9.3" authors = [ { name="Vincent Danen", email="vdanen@annvix.com" }, ]