diff --git a/.github/workflows/python_test.yml b/.github/workflows/python_test.yml index 3177d09a0..f9faf7625 100644 --- a/.github/workflows/python_test.yml +++ b/.github/workflows/python_test.yml @@ -70,12 +70,19 @@ jobs: - name: Install the project run: uv sync + - name: Generate dynamic coverage configuration + run: | + echo "Generating dynamic coverage configuration..." + python3 scripts/generate_coverage_config.py + echo "Generated .coveragerc with the following exclusions:" + echo "Bundled components and legacy files excluded from coverage" + - name: Run unit tests uses: nick-fields/retry@v3 with: timeout_minutes: 12 max_attempts: 2 - command: make unit_tests args="-x -vv --splits ${{ matrix.splitCount }} --group ${{ matrix.group }} --reruns 5 --cov --cov-report=xml --cov-report=html" + command: make unit_tests args="-x -vv --splits ${{ matrix.splitCount }} --group ${{ matrix.group }} --reruns 5 --cov --cov-config=src/backend/.coveragerc --cov-report=xml --cov-report=html" - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 diff --git a/codecov.yml b/codecov.yml index 371ddee8d..749523966 100644 --- a/codecov.yml +++ b/codecov.yml @@ -66,6 +66,22 @@ flags: - src/frontend/ carryforward: true # Preserve coverage data across builds if missing +# Define coverage components for granular reporting +component_management: + default_rules: # default rules that will be inherited by all components + statuses: + - type: project # in this case every component that doens't have a status defined will have a project type one + target: auto + branches: + - "!main" + individual_components: + - component_id: backend_components + name: "Backend Components" + paths: + - src/backend/base/langflow/components/** + # Note: Many components excluded by .coveragerc (bundled + legacy) + # This tracks coverage of remaining "core" components only + # Files/directories to exclude from coverage calculations ignore: # Database migrations - infrastructure code, not business logic diff --git a/scripts/generate_coverage_config.md b/scripts/generate_coverage_config.md new file mode 100644 index 000000000..03c1bb333 --- /dev/null +++ b/scripts/generate_coverage_config.md @@ -0,0 +1,36 @@ +# Dynamic Coverage Configuration + +This script generates a custom `.coveragerc` file for backend testing that excludes: + +1. **Bundled components** - Components listed in `SIDEBAR_BUNDLES` from `src/frontend/src/utils/styleUtils.ts` +2. **Legacy components** - Python files containing `legacy = True` + +## How it works + +1. **Reads frontend config**: Parses `styleUtils.ts` to extract bundled component names +2. **Scans backend files**: Finds all `.py` files in `src/backend/base/langflow/components/` with `legacy = True` +3. **Generates .coveragerc**: Creates exclusion patterns for pytest-cov + +## Usage + +### Local development +```bash +# Generate config and run tests +python3 scripts/generate_coverage_config.py +cd src/backend && python -m pytest --cov=src/backend/base/langflow --cov-config=.coveragerc +``` + +### CI Integration +The script runs automatically in CI before backend tests via `.github/workflows/python_test.yml`. + +## Files affected + +- **Input**: `src/frontend/src/utils/styleUtils.ts` (SIDEBAR_BUNDLES) +- **Input**: `src/backend/base/langflow/components/**/*.py` (legacy components) +- **Output**: `src/backend/.coveragerc` (auto-generated, in .gitignore) + +## Benefits + +- **Accurate coverage**: Focuses on actively maintained core code +- **Dynamic updates**: Automatically adapts when bundles/legacy components change +- **Codecov compatible**: Generated config works with both local testing and Codecov reporting \ No newline at end of file diff --git a/scripts/generate_coverage_config.py b/scripts/generate_coverage_config.py new file mode 100755 index 000000000..1081bf223 --- /dev/null +++ b/scripts/generate_coverage_config.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Script to generate a custom .coveragerc file for backend testing. + +This script: +1. Reads SIDEBAR_BUNDLES from frontend styleUtils.ts to get bundled component names +2. Scans backend components for files containing 'legacy = True' +3. Generates a .coveragerc file that omits these paths from coverage reporting + +Usage: + python scripts/generate_coverage_config.py +""" + +import re +from pathlib import Path + + +def extract_sidebar_bundles(frontend_path: Path) -> set[str]: + """Extract component names from SIDEBAR_BUNDLES in styleUtils.ts.""" + style_utils_path = frontend_path / "src/utils/styleUtils.ts" + + if not style_utils_path.exists(): + print(f"Warning: styleUtils.ts not found at {style_utils_path}") + return set() + + bundle_names = set() + + with style_utils_path.open(encoding="utf-8") as f: + content = f.read() + + # Find SIDEBAR_BUNDLES array + sidebar_match = re.search(r"export const SIDEBAR_BUNDLES = \[(.*?)\];", content, re.DOTALL) + if not sidebar_match: + print("Warning: SIDEBAR_BUNDLES not found in styleUtils.ts") + return set() + + bundles_content = sidebar_match.group(1) + + # Extract name fields using regex + name_matches = re.findall(r'name:\s*["\']([^"\']+)["\']', bundles_content) + + for name in name_matches: + bundle_names.add(name) + + print(f"Found {len(bundle_names)} bundled components from SIDEBAR_BUNDLES") + return bundle_names + + +def find_legacy_components(backend_components_path: Path) -> set[str]: + """Find Python files containing 'legacy = True'.""" + legacy_files = set() + + if not backend_components_path.exists(): + print(f"Warning: Backend components path not found: {backend_components_path}") + return set() + + # Walk through all Python files in components directory + for py_file in backend_components_path.rglob("*.py"): + try: + with py_file.open(encoding="utf-8") as f: + content = f.read() + + # Check if file contains 'legacy = True' + if re.search(r"\blegacy\s*=\s*True\b", content): + # Get relative path from components directory + rel_path = py_file.relative_to(backend_components_path) + legacy_files.add(str(rel_path)) + + except (UnicodeDecodeError, PermissionError) as e: + print(f"Warning: Could not read {py_file}: {e}") + continue + + print(f"Found {len(legacy_files)} legacy component files") + return legacy_files + + +def generate_coveragerc(bundle_names: set[str], legacy_files: set[str], output_path: Path): + """Generate .coveragerc file with omit patterns.""" + # Base coveragerc content + config_content = """# Auto-generated .coveragerc file +# Generated by scripts/generate_coverage_config.py +# Do not edit manually - changes will be overwritten + +[run] +source = src/backend/base/langflow +omit = + # Test files + */tests/* + */test_* + */*test* + + # Migration files + */alembic/* + */migrations/* + + # Cache and build files + */__pycache__/* + */.* + + # Init files (typically just imports) + */__init__.py + + # Deactivate Components + */components/deactivated/* + +""" + + # Add bundled components to omit list + if bundle_names: + config_content += " # Bundled components from SIDEBAR_BUNDLES\n" + for bundle_name in sorted(bundle_names): + config_content += f" */components/{bundle_name}/*\n" + config_content += "\n" + + # Add legacy components to omit list + if legacy_files: + config_content += " # Legacy components (contain 'legacy = True')\n" + for legacy_file in sorted(legacy_files): + # Convert relative path to omit pattern + omit_pattern = f" */components/{legacy_file}\n" + config_content += omit_pattern + + config_content += """ +# Note: [report] and [html] sections omitted for Codecov compatibility +# Codecov handles its own reporting and ignores these sections +""" + + # Write the config file + output_path.parent.mkdir(parents=True, exist_ok=True) + with output_path.open("w", encoding="utf-8") as f: + f.write(config_content) + + print(f"Generated .coveragerc at {output_path}") + print(f" - Omitting {len(bundle_names)} bundled component directories") + print(f" - Omitting {len(legacy_files)} legacy component files") + + +def main(): + """Main function.""" + # Determine project root (script is in scripts/ directory) + script_dir = Path(__file__).parent + project_root = script_dir.parent + + # Paths + frontend_path = project_root / "src" / "frontend" + backend_components_path = project_root / "src" / "backend" / "base" / "langflow" / "components" + output_path = project_root / "src" / "backend" / ".coveragerc" + + print(f"Project root: {project_root}") + print(f"Frontend path: {frontend_path}") + print(f"Backend components path: {backend_components_path}") + print(f"Output path: {output_path}") + print() + + # Extract bundled component names + bundle_names = extract_sidebar_bundles(frontend_path) + + # Find legacy components + legacy_files = find_legacy_components(backend_components_path) + + # Generate .coveragerc file + generate_coveragerc(bundle_names, legacy_files, output_path) + + print("\nDone! You can now run backend tests with coverage using:") + print("cd src/backend && python -m pytest --cov=src/backend/base/langflow --cov-config=.coveragerc") + + +if __name__ == "__main__": + main() diff --git a/src/backend/.gitignore b/src/backend/.gitignore index ac0cc6c6d..831545957 100644 --- a/src/backend/.gitignore +++ b/src/backend/.gitignore @@ -131,4 +131,7 @@ dmypy.json # Pyre type checker .pyre/ -*.db \ No newline at end of file +*.db + +# Generated pytest-cov config +.coveragerc