Files
LEDMatrix/docs/HOW_TO_RUN_TESTS.md
Chuck 781224591f fix: post-audit follow-up code fixes (cache, fonts, icons, dev script) (#307)
* fix: post-audit follow-up code fixes (cache, fonts, icons, dev script, CI)

The docs refresh effort (#306, ledmatrix-plugins#92) surfaced seven
code bugs that were intentionally left out of the docs PRs because
they required code changes rather than doc fixes. This PR addresses
the six that belong in LEDMatrix (the seventh — a lacrosse-scoreboard
mode rename — lives in the plugins repo).

Bug 1: cache_manager.delete() AttributeError
  src/common/api_helper.py:287 and
  src/plugin_system/resource_monitor.py:343 both call
  cache_manager.delete(key), which doesn't exist — only
  clear_cache(key=None). Added a delete() alias method on
  CacheManager that forwards to clear_cache(key). Reverts the
  "There is no delete() method" wording in DEVELOPER_QUICK_REFERENCE,
  .cursorrules so the docs match the new shim.

Bug 2: dev_plugin_setup.sh PROJECT_ROOT resolution
  scripts/dev/dev_plugin_setup.sh:9 set PROJECT_ROOT to SCRIPT_DIR
  instead of walking up two levels to the repo root, so PLUGINS_DIR
  resolved to scripts/dev/plugins/ and created symlinks under the
  script's own directory. Fixed the path and removed the stray
  scripts/dev/plugins/of-the-day symlink left by earlier runs.

Bug 3: plugin custom icons regressed from v2 to v3
  web_interface/blueprints/api_v3.py built the /plugins/installed
  response without including the manifest's "icon" field, and
  web_interface/templates/v3/base.html hardcoded
  fas fa-puzzle-piece in all three plugin-tab render sites. Pass
  the icon through the API and read it from the templates with a
  puzzle-piece fallback. Reverts the "currently broken" banners in
  docs/PLUGIN_CUSTOM_ICONS.md and docs/PLUGIN_CUSTOM_ICONS_FEATURE.md.

Bug 4: register_plugin_fonts was never wired up
  src/font_manager.py:150 defines register_plugin_fonts(plugin_id,
  font_manifest) but nothing called it, so plugin manifests with a
  "fonts" block were silently no-ops. Wired the call into
  PluginManager.load_plugin() right after plugin_loader.load_plugin
  returns. Reverts the "not currently wired" warning in
  docs/FONT_MANAGER.md's "For Plugin Developers" section.

Bug 5: dead web_interface_v2 import pattern (LEDMatrix half)
  src/base_odds_manager.py had a try/except importing
  web_interface_v2.increment_api_counter, falling back to a no-op
  stub. The module doesn't exist anywhere in the v3 codebase and
  no API metrics dashboard reads it. Deleted the import block and
  the single call site; the plugins-repo half of this cleanup lands
  in ledmatrix-plugins#<next>.

Bug 7: no CI test workflow
  .github/workflows/ only contained security-audit.yml; pytest ran
  locally but was not gated on PRs. Added
  .github/workflows/tests.yml running pytest against Python 3.10,
  3.11, 3.12 in EMULATOR=true mode, skipping tests marked hardware
  or slow. Updated docs/HOW_TO_RUN_TESTS.md to reflect that the
  workflow now exists.

Verification done locally:
  - CacheManager.delete(key) round-trips with set/get
  - base_odds_manager imports without the v2 module present
  - dev_plugin_setup.sh PROJECT_ROOT resolves to repo root
  - api_v3 and plugin_manager compile clean
  - tests.yml YAML parses
  - Script syntax check on dev_plugin_setup.sh

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review comments on #307

- src/cache_manager.py: clear_cache(key) treated empty string as
  "wipe all" because of `if key:`. Switched to `key is None`
  branching, made delete(key) and clear_cache(key) reject empty
  strings and None outright with ValueError, and updated both
  docstrings to make the contract explicit. Verified locally
  with a round-trip test that clear_cache() (no arg) still
  wipes everything but clear_cache("") and delete("") raise.

- src/plugin_system/plugin_manager.py: was reaching for the
  font manager via getattr(self.display_manager, 'font_manager',
  None). PluginManager already takes a dedicated font_manager
  parameter (line 54) and stores it as self.font_manager
  (line 69), so the old path was both wrong and could miss the
  font manager entirely when the host injects them separately.
  Switched to self.font_manager directly with the same try/except
  warning behavior.

- web_interface/templates/v3/base.html: in the full plugin-tab
  renderer, the icon was injected with
  `<i class="${escapeHtml(plugin.icon)}">` — but escapeHtml only
  escapes <, >, and &, not double quotes, so a manifest with a
  quote in its icon string could break out of the class
  attribute. Replaced the innerHTML template with createElement
  for the <i> tag, set className from plugin.icon directly
  (no string interpolation), and used a text node for the
  label. Same fix shape would also harden the two stub-renderer
  sites at line 515 / 774, but those already escape `"` to
  &quot; and CodeRabbit only flagged this site, so leaving them
  for now.

- docs/FONT_MANAGER.md: clarified that the Manual Font Overrides
  *workflow* (set_override / remove_override / font_overrides.json)
  is the supported override path today, and only the Fonts tab
  in the web UI is the placeholder. Previous wording conflated
  the two and made it sound like overrides themselves were
  broken.

- docs/HOW_TO_RUN_TESTS.md: replaced the vague "see the PR
  adding it" with a concrete link to #307 and a note that the
  workflow file itself is held back pending the workflow scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 09:25:12 -04:00

386 lines
9.1 KiB
Markdown

# How to Run Tests for LEDMatrix
This guide explains how to use the test suite for the LEDMatrix project.
## Prerequisites
### 1. Install Test Dependencies
Make sure you have the testing packages installed:
```bash
# Install all dependencies including test packages
pip install -r requirements.txt
# Or install just the test dependencies
pip install pytest pytest-cov pytest-mock
```
### 2. Set Environment Variables
For tests that don't require hardware, set the emulator mode:
```bash
export EMULATOR=true
```
This ensures tests use the emulator instead of trying to access actual hardware.
## Running Tests
### Run All Tests
```bash
# From the project root directory
pytest
# Or with more verbose output
pytest -v
# Or with even more detail
pytest -vv
```
### Run Specific Test Files
```bash
# Run a specific test file
pytest test/test_display_controller.py
# Run multiple specific files
pytest test/test_display_controller.py test/test_plugin_system.py
```
### Run Specific Test Classes or Functions
```bash
# Run a specific test class
pytest test/test_display_controller.py::TestDisplayControllerModeRotation
# Run a specific test function
pytest test/test_display_controller.py::TestDisplayControllerModeRotation::test_basic_rotation
```
### Run Tests by Marker
The tests use markers to categorize them:
```bash
# Run only unit tests (fast, isolated)
pytest -m unit
# Run only integration tests
pytest -m integration
# Run tests that don't require hardware
pytest -m "not hardware"
# Run slow tests
pytest -m slow
```
### Run Tests in a Directory
```bash
# Run all tests in the test directory
pytest test/
# Run plugin tests only
pytest test/plugins/
# Run web interface tests only
pytest test/web_interface/
# Run web interface integration tests
pytest test/web_interface/integration/
```
## Understanding Test Output
### Basic Output
When you run `pytest`, you'll see:
```
test/test_display_controller.py::TestDisplayControllerInitialization::test_init_success PASSED
test/test_display_controller.py::TestDisplayControllerModeRotation::test_basic_rotation PASSED
...
```
- `PASSED` - Test succeeded
- `FAILED` - Test failed (check the error message)
- `SKIPPED` - Test was skipped (usually due to missing dependencies or conditions)
- `ERROR` - Test had an error during setup
### Verbose Output
Use `-v` or `-vv` for more detail:
```bash
pytest -vv
```
This shows:
- Full test names
- Setup/teardown information
- More detailed failure messages
### Show Print Statements
To see print statements and logging output:
```bash
pytest -s
```
Or combine with verbose:
```bash
pytest -sv
```
## Coverage Reports
The test suite is configured to generate coverage reports.
### View Coverage in Terminal
```bash
# Coverage is automatically shown when running pytest
pytest
# The output will show something like:
# ----------- coverage: platform linux, python 3.11.5 -----------
# Name Stmts Miss Cover Missing
# ---------------------------------------------------------------------
# src/display_controller.py 450 120 73% 45-67, 89-102
```
### Generate HTML Coverage Report
```bash
# HTML report is automatically generated in htmlcov/
pytest
# Then open the report in your browser
# On Linux:
xdg-open htmlcov/index.html
# On macOS:
open htmlcov/index.html
# On Windows:
start htmlcov/index.html
```
The HTML report shows:
- Line-by-line coverage
- Files with low coverage highlighted
- Interactive navigation
### Coverage Threshold
The tests are configured to fail if coverage drops below 30%. To change this, edit `pytest.ini`:
```ini
--cov-fail-under=30 # Change this value
```
## Common Test Scenarios
### Run Tests After Making Changes
```bash
# Quick test run (just unit tests)
pytest -m unit
# Full test suite
pytest
```
### Debug a Failing Test
```bash
# Run with maximum verbosity and show print statements
pytest -vv -s test/test_display_controller.py::TestDisplayControllerModeRotation::test_basic_rotation
# Run with Python debugger (pdb)
pytest --pdb test/test_display_controller.py::TestDisplayControllerModeRotation::test_basic_rotation
```
### Run Tests in Parallel (Faster)
```bash
# Install pytest-xdist first
pip install pytest-xdist
# Run tests in parallel (4 workers)
pytest -n 4
# Auto-detect number of CPUs
pytest -n auto
```
### Stop on First Failure
```bash
# Stop immediately when a test fails
pytest -x
# Stop after N failures
pytest --maxfail=3
```
## Test Organization
### Test Files Structure
```
test/
├── conftest.py # Shared fixtures and configuration
├── test_display_controller.py # Display controller tests
├── test_display_manager.py # Display manager tests
├── test_plugin_system.py # Plugin system tests
├── test_plugin_loader.py # Plugin discovery/loading tests
├── test_plugin_loading_failures.py # Plugin failure-mode tests
├── test_cache_manager.py # Cache manager tests
├── test_config_manager.py # Config manager tests
├── test_config_service.py # Config service tests
├── test_config_validation_edge_cases.py # Config edge cases
├── test_font_manager.py # Font manager tests
├── test_layout_manager.py # Layout manager tests
├── test_text_helper.py # Text helper tests
├── test_error_handling.py # Error handling tests
├── test_error_aggregator.py # Error aggregation tests
├── test_schema_manager.py # Schema manager tests
├── test_web_api.py # Web API tests
├── test_nba_*.py # NBA-specific test suites
├── plugins/ # Per-plugin test suites
│ ├── test_clock_simple.py
│ ├── test_calendar.py
│ ├── test_basketball_scoreboard.py
│ ├── test_soccer_scoreboard.py
│ ├── test_odds_ticker.py
│ ├── test_text_display.py
│ ├── test_visual_rendering.py
│ └── test_plugin_base.py
└── web_interface/
├── test_config_manager_atomic.py
├── test_state_reconciliation.py
├── test_plugin_operation_queue.py
├── test_dedup_unique_arrays.py
└── integration/ # Web interface integration tests
├── test_config_flows.py
└── test_plugin_operations.py
```
### Test Categories
- **Unit Tests**: Fast, isolated tests for individual components
- **Integration Tests**: Tests that verify components work together
- **Error Scenarios**: Tests for error handling and edge cases
- **Edge Cases**: Boundary conditions and unusual inputs
## Troubleshooting
### Import Errors
If you see import errors:
```bash
# Make sure you're in the project root
cd /home/chuck/Github/LEDMatrix
# Check Python path
python -c "import sys; print(sys.path)"
# Run pytest from project root
pytest
```
### Missing Dependencies
If tests fail due to missing packages:
```bash
# Install all dependencies
pip install -r requirements.txt
# Or install specific missing package
pip install <package-name>
```
### Hardware Tests Failing
If tests that require hardware are failing:
```bash
# Set emulator mode
export EMULATOR=true
# Or skip hardware tests
pytest -m "not hardware"
```
### Coverage Not Working
If coverage reports aren't generating:
```bash
# Make sure pytest-cov is installed
pip install pytest-cov
# Run with explicit coverage
pytest --cov=src --cov-report=html
```
## Continuous Integration
The repo runs
[`.github/workflows/security-audit.yml`](../.github/workflows/security-audit.yml)
(bandit + semgrep) on every push. A pytest CI workflow at
`.github/workflows/tests.yml` is queued to land alongside this
PR ([ChuckBuilds/LEDMatrix#307](https://github.com/ChuckBuilds/LEDMatrix/pull/307));
the workflow file itself was held back from that PR because the
push token lacked the GitHub `workflow` scope, so it needs to be
committed separately by a maintainer. Once it's in, this section
will be updated to describe what the job runs.
## Best Practices
1. **Run tests before committing**:
```bash
pytest -m unit # Quick check
```
2. **Run full suite before pushing**:
```bash
pytest # Full test suite with coverage
```
3. **Fix failing tests immediately** - Don't let them accumulate
4. **Keep coverage above threshold** - Aim for 70%+ coverage
5. **Write tests for new features** - Add tests when adding new functionality
## Quick Reference
```bash
# Most common commands
pytest # Run all tests with coverage
pytest -v # Verbose output
pytest -m unit # Run only unit tests
pytest -k "test_name" # Run tests matching pattern
pytest --cov=src # Generate coverage report
pytest -x # Stop on first failure
pytest --pdb # Drop into debugger on failure
```
## Getting Help
- Check test output for error messages
- Look at the test file to understand what's being tested
- Check `conftest.py` for available fixtures
- Review `pytest.ini` for configuration options