#!/bin/bash # Run ArchiveBox plugin tests with coverage # # All plugin tests use pytest and are located in pluginname/tests/test_*.py # # Usage: ./bin/test_plugins.sh [plugin_name] [--no-coverage] [--coverage-report] # # Examples: # ./bin/test_plugins.sh # Run all plugin tests with coverage # ./bin/test_plugins.sh chrome # Run chrome plugin tests with coverage # ./bin/test_plugins.sh parse_* # Run all parse_* plugin tests with coverage # ./bin/test_plugins.sh --no-coverage # Run all tests without coverage # ./bin/test_plugins.sh --coverage-report # Just show coverage report without running tests # # For running individual hooks with coverage: # NODE_V8_COVERAGE=./coverage/js node .js [args] # JS hooks # coverage run --parallel-mode .py [args] # Python hooks # # Coverage results are saved to .coverage (Python) and coverage/js (JavaScript): # coverage combine && coverage report # coverage json # ./bin/test_plugins.sh --coverage-report set -e # Color codes GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Save root directory first ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" # Parse arguments PLUGIN_FILTER="" ENABLE_COVERAGE=true COVERAGE_REPORT_ONLY=false for arg in "$@"; do if [ "$arg" = "--no-coverage" ]; then ENABLE_COVERAGE=false elif [ "$arg" = "--coverage-report" ]; then COVERAGE_REPORT_ONLY=true else PLUGIN_FILTER="$arg" fi done # Function to show JS coverage report (inlined from convert_v8_coverage.js) show_js_coverage() { local coverage_dir="$1" if [ ! -d "$coverage_dir" ] || [ -z "$(ls -A "$coverage_dir" 2>/dev/null)" ]; then echo "No JavaScript coverage data collected" echo "(JS hooks may not have been executed during tests)" return fi node - "$coverage_dir" << 'ENDJS' const fs = require('fs'); const path = require('path'); const coverageDir = process.argv[2]; const files = fs.readdirSync(coverageDir).filter(f => f.startsWith('coverage-') && f.endsWith('.json')); if (files.length === 0) { console.log('No coverage files found'); process.exit(0); } const coverageByFile = {}; files.forEach(file => { const data = JSON.parse(fs.readFileSync(path.join(coverageDir, file), 'utf8')); data.result.forEach(script => { const url = script.url; if (url.startsWith('node:') || url.includes('node_modules')) return; if (!coverageByFile[url]) { coverageByFile[url] = { totalRanges: 0, executedRanges: 0 }; } script.functions.forEach(func => { func.ranges.forEach(range => { coverageByFile[url].totalRanges++; if (range.count > 0) coverageByFile[url].executedRanges++; }); }); }); }); const allFiles = Object.keys(coverageByFile).sort(); const pluginFiles = allFiles.filter(url => url.includes('archivebox/plugins')); const otherFiles = allFiles.filter(url => !url.startsWith('node:') && !url.includes('archivebox/plugins')); console.log('Total files with coverage: ' + allFiles.length + '\n'); console.log('Plugin files: ' + pluginFiles.length); console.log('Node internal: ' + allFiles.filter(u => u.startsWith('node:')).length); console.log('Other: ' + otherFiles.length + '\n'); console.log('JavaScript Coverage Report'); console.log('='.repeat(80)); console.log(''); if (otherFiles.length > 0) { console.log('Non-plugin files with coverage:'); otherFiles.forEach(url => console.log(' ' + url)); console.log(''); } if (pluginFiles.length === 0) { console.log('No plugin files covered'); process.exit(0); } let totalRanges = 0, totalExecuted = 0; pluginFiles.forEach(url => { const cov = coverageByFile[url]; const pct = cov.totalRanges > 0 ? (cov.executedRanges / cov.totalRanges * 100).toFixed(1) : '0.0'; const match = url.match(/archivebox\/plugins\/.+/); const displayPath = match ? match[0] : url; console.log(displayPath + ': ' + pct + '% (' + cov.executedRanges + '/' + cov.totalRanges + ' ranges)'); totalRanges += cov.totalRanges; totalExecuted += cov.executedRanges; }); console.log(''); console.log('-'.repeat(80)); const overallPct = totalRanges > 0 ? (totalExecuted / totalRanges * 100).toFixed(1) : '0.0'; console.log('Total: ' + overallPct + '% (' + totalExecuted + '/' + totalRanges + ' ranges)'); ENDJS } # If --coverage-report only, just show the report and exit if [ "$COVERAGE_REPORT_ONLY" = true ]; then cd "$ROOT_DIR" || exit 1 echo "==========================================" echo "Python Coverage Summary" echo "==========================================" coverage combine 2>/dev/null || true coverage report --include="archivebox/plugins/*" --omit="*/tests/*" echo "" echo "==========================================" echo "JavaScript Coverage Summary" echo "==========================================" show_js_coverage "$ROOT_DIR/coverage/js" echo "" echo "For detailed coverage reports:" echo " Python: coverage report --show-missing --include='archivebox/plugins/*' --omit='*/tests/*'" echo " Python: coverage json # LLM-friendly format" echo " Python: coverage html # Interactive HTML report" exit 0 fi # Set DATA_DIR for tests (required by abx_pkg and plugins) # Use temp dir to isolate tests from project files if [ -z "$DATA_DIR" ]; then export DATA_DIR=$(mktemp -d -t archivebox_plugin_tests.XXXXXX) # Clean up on exit trap "rm -rf '$DATA_DIR'" EXIT fi # Reset coverage data if collecting coverage if [ "$ENABLE_COVERAGE" = true ]; then echo "Resetting coverage data..." cd "$ROOT_DIR" || exit 1 coverage erase rm -rf "$ROOT_DIR/coverage/js" 2>/dev/null mkdir -p "$ROOT_DIR/coverage/js" # Enable Python subprocess coverage export COVERAGE_PROCESS_START="$ROOT_DIR/pyproject.toml" export PYTHONPATH="$ROOT_DIR:$PYTHONPATH" # For sitecustomize.py # Enable Node.js V8 coverage (built-in, no packages needed) export NODE_V8_COVERAGE="$ROOT_DIR/coverage/js" echo "Python coverage: enabled (subprocess support)" echo "JavaScript coverage: enabled (NODE_V8_COVERAGE)" echo "" fi # Change to plugins directory cd "$ROOT_DIR/archivebox/plugins" || exit 1 echo "==========================================" echo "ArchiveBox Plugin Tests" echo "==========================================" echo "" if [ -n "$PLUGIN_FILTER" ]; then echo "Filter: $PLUGIN_FILTER" else echo "Running all plugin tests" fi if [ "$ENABLE_COVERAGE" = true ]; then echo "Coverage: enabled" else echo "Coverage: disabled" fi echo "" # Track results TOTAL_PLUGINS=0 PASSED_PLUGINS=0 FAILED_PLUGINS=0 # Find and run plugin tests if [ -n "$PLUGIN_FILTER" ]; then # Run tests for specific plugin(s) matching pattern TEST_DIRS=$(find . -maxdepth 2 -type d -path "./${PLUGIN_FILTER}*/tests" 2>/dev/null | sort) else # Run all plugin tests TEST_DIRS=$(find . -maxdepth 2 -type d -name "tests" -path "./*/tests" 2>/dev/null | sort) fi if [ -z "$TEST_DIRS" ]; then echo -e "${YELLOW}No plugin tests found${NC}" [ -n "$PLUGIN_FILTER" ] && echo "Pattern: $PLUGIN_FILTER" exit 0 fi for test_dir in $TEST_DIRS; do # Check if there are any Python test files if ! compgen -G "${test_dir}/test_*.py" > /dev/null 2>&1; then continue fi plugin_name=$(basename $(dirname "$test_dir")) TOTAL_PLUGINS=$((TOTAL_PLUGINS + 1)) echo -e "${YELLOW}[RUNNING]${NC} $plugin_name" # Build pytest command with optional coverage PYTEST_CMD="python -m pytest $test_dir -p no:django -v --tb=short" if [ "$ENABLE_COVERAGE" = true ]; then PYTEST_CMD="$PYTEST_CMD --cov=$plugin_name --cov-append --cov-branch" fi if eval "$PYTEST_CMD" 2>&1 | grep -v "^platform\|^cachedir\|^rootdir\|^configfile\|^plugins:" | tail -100; then echo -e "${GREEN}[PASSED]${NC} $plugin_name" PASSED_PLUGINS=$((PASSED_PLUGINS + 1)) else echo -e "${RED}[FAILED]${NC} $plugin_name" FAILED_PLUGINS=$((FAILED_PLUGINS + 1)) fi echo "" done # Print summary echo "==========================================" echo "Test Summary" echo "==========================================" echo -e "Total plugins tested: $TOTAL_PLUGINS" echo -e "${GREEN}Passed:${NC} $PASSED_PLUGINS" echo -e "${RED}Failed:${NC} $FAILED_PLUGINS" echo "" if [ $TOTAL_PLUGINS -eq 0 ]; then echo -e "${YELLOW}⚠ No tests found${NC}" exit 0 elif [ $FAILED_PLUGINS -eq 0 ]; then echo -e "${GREEN}✓ All plugin tests passed!${NC}" # Show coverage summary if enabled if [ "$ENABLE_COVERAGE" = true ]; then echo "" echo "==========================================" echo "Python Coverage Summary" echo "==========================================" # Coverage data is in ROOT_DIR, combine and report from there cd "$ROOT_DIR" || exit 1 # Copy coverage data from plugins dir if it exists if [ -f "$ROOT_DIR/archivebox/plugins/.coverage" ]; then cp "$ROOT_DIR/archivebox/plugins/.coverage" "$ROOT_DIR/.coverage" fi coverage combine 2>/dev/null || true coverage report --include="archivebox/plugins/*" --omit="*/tests/*" 2>&1 | head -50 echo "" echo "==========================================" echo "JavaScript Coverage Summary" echo "==========================================" show_js_coverage "$ROOT_DIR/coverage/js" echo "" echo "For detailed coverage reports (from project root):" echo " Python: coverage report --show-missing --include='archivebox/plugins/*' --omit='*/tests/*'" echo " Python: coverage json # LLM-friendly format" echo " Python: coverage html # Interactive HTML report" echo " JavaScript: ./bin/test_plugins.sh --coverage-report" fi exit 0 else echo -e "${RED}✗ Some plugin tests failed${NC}" exit 1 fi