test_plugins.sh 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. #!/bin/bash
  2. # Run ArchiveBox plugin tests with coverage
  3. #
  4. # All plugin tests use pytest and are located in pluginname/tests/test_*.py
  5. #
  6. # Usage: ./bin/test_plugins.sh [plugin_name] [--no-coverage] [--coverage-report]
  7. #
  8. # Examples:
  9. # ./bin/test_plugins.sh # Run all plugin tests with coverage
  10. # ./bin/test_plugins.sh chrome # Run chrome plugin tests with coverage
  11. # ./bin/test_plugins.sh parse_* # Run all parse_* plugin tests with coverage
  12. # ./bin/test_plugins.sh --no-coverage # Run all tests without coverage
  13. # ./bin/test_plugins.sh --coverage-report # Just show coverage report without running tests
  14. #
  15. # For running individual hooks with coverage:
  16. # NODE_V8_COVERAGE=./coverage/js node <hook>.js [args] # JS hooks
  17. # coverage run --parallel-mode <hook>.py [args] # Python hooks
  18. #
  19. # Coverage results are saved to .coverage (Python) and coverage/js (JavaScript):
  20. # coverage combine && coverage report
  21. # coverage json
  22. # ./bin/test_plugins.sh --coverage-report
  23. set -e
  24. # Color codes
  25. GREEN='\033[0;32m'
  26. RED='\033[0;31m'
  27. YELLOW='\033[1;33m'
  28. NC='\033[0m' # No Color
  29. # Save root directory first
  30. ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
  31. # Parse arguments
  32. PLUGIN_FILTER=""
  33. ENABLE_COVERAGE=true
  34. COVERAGE_REPORT_ONLY=false
  35. for arg in "$@"; do
  36. if [ "$arg" = "--no-coverage" ]; then
  37. ENABLE_COVERAGE=false
  38. elif [ "$arg" = "--coverage-report" ]; then
  39. COVERAGE_REPORT_ONLY=true
  40. else
  41. PLUGIN_FILTER="$arg"
  42. fi
  43. done
  44. # Function to show JS coverage report (inlined from convert_v8_coverage.js)
  45. show_js_coverage() {
  46. local coverage_dir="$1"
  47. if [ ! -d "$coverage_dir" ] || [ -z "$(ls -A "$coverage_dir" 2>/dev/null)" ]; then
  48. echo "No JavaScript coverage data collected"
  49. echo "(JS hooks may not have been executed during tests)"
  50. return
  51. fi
  52. node - "$coverage_dir" << 'ENDJS'
  53. const fs = require('fs');
  54. const path = require('path');
  55. const coverageDir = process.argv[2];
  56. const files = fs.readdirSync(coverageDir).filter(f => f.startsWith('coverage-') && f.endsWith('.json'));
  57. if (files.length === 0) {
  58. console.log('No coverage files found');
  59. process.exit(0);
  60. }
  61. const coverageByFile = {};
  62. files.forEach(file => {
  63. const data = JSON.parse(fs.readFileSync(path.join(coverageDir, file), 'utf8'));
  64. data.result.forEach(script => {
  65. const url = script.url;
  66. if (url.startsWith('node:') || url.includes('node_modules')) return;
  67. if (!coverageByFile[url]) {
  68. coverageByFile[url] = { totalRanges: 0, executedRanges: 0 };
  69. }
  70. script.functions.forEach(func => {
  71. func.ranges.forEach(range => {
  72. coverageByFile[url].totalRanges++;
  73. if (range.count > 0) coverageByFile[url].executedRanges++;
  74. });
  75. });
  76. });
  77. });
  78. const allFiles = Object.keys(coverageByFile).sort();
  79. const pluginFiles = allFiles.filter(url => url.includes('archivebox/plugins'));
  80. const otherFiles = allFiles.filter(url => !url.startsWith('node:') && !url.includes('archivebox/plugins'));
  81. console.log('Total files with coverage: ' + allFiles.length + '\n');
  82. console.log('Plugin files: ' + pluginFiles.length);
  83. console.log('Node internal: ' + allFiles.filter(u => u.startsWith('node:')).length);
  84. console.log('Other: ' + otherFiles.length + '\n');
  85. console.log('JavaScript Coverage Report');
  86. console.log('='.repeat(80));
  87. console.log('');
  88. if (otherFiles.length > 0) {
  89. console.log('Non-plugin files with coverage:');
  90. otherFiles.forEach(url => console.log(' ' + url));
  91. console.log('');
  92. }
  93. if (pluginFiles.length === 0) {
  94. console.log('No plugin files covered');
  95. process.exit(0);
  96. }
  97. let totalRanges = 0, totalExecuted = 0;
  98. pluginFiles.forEach(url => {
  99. const cov = coverageByFile[url];
  100. const pct = cov.totalRanges > 0 ? (cov.executedRanges / cov.totalRanges * 100).toFixed(1) : '0.0';
  101. const match = url.match(/archivebox\/plugins\/.+/);
  102. const displayPath = match ? match[0] : url;
  103. console.log(displayPath + ': ' + pct + '% (' + cov.executedRanges + '/' + cov.totalRanges + ' ranges)');
  104. totalRanges += cov.totalRanges;
  105. totalExecuted += cov.executedRanges;
  106. });
  107. console.log('');
  108. console.log('-'.repeat(80));
  109. const overallPct = totalRanges > 0 ? (totalExecuted / totalRanges * 100).toFixed(1) : '0.0';
  110. console.log('Total: ' + overallPct + '% (' + totalExecuted + '/' + totalRanges + ' ranges)');
  111. ENDJS
  112. }
  113. # If --coverage-report only, just show the report and exit
  114. if [ "$COVERAGE_REPORT_ONLY" = true ]; then
  115. cd "$ROOT_DIR" || exit 1
  116. echo "=========================================="
  117. echo "Python Coverage Summary"
  118. echo "=========================================="
  119. coverage combine 2>/dev/null || true
  120. coverage report --include="archivebox/plugins/*" --omit="*/tests/*"
  121. echo ""
  122. echo "=========================================="
  123. echo "JavaScript Coverage Summary"
  124. echo "=========================================="
  125. show_js_coverage "$ROOT_DIR/coverage/js"
  126. echo ""
  127. echo "For detailed coverage reports:"
  128. echo " Python: coverage report --show-missing --include='archivebox/plugins/*' --omit='*/tests/*'"
  129. echo " Python: coverage json # LLM-friendly format"
  130. echo " Python: coverage html # Interactive HTML report"
  131. exit 0
  132. fi
  133. # Set DATA_DIR for tests (required by abx_pkg and plugins)
  134. # Use temp dir to isolate tests from project files
  135. if [ -z "$DATA_DIR" ]; then
  136. export DATA_DIR=$(mktemp -d -t archivebox_plugin_tests.XXXXXX)
  137. # Clean up on exit
  138. trap "rm -rf '$DATA_DIR'" EXIT
  139. fi
  140. # Reset coverage data if collecting coverage
  141. if [ "$ENABLE_COVERAGE" = true ]; then
  142. echo "Resetting coverage data..."
  143. cd "$ROOT_DIR" || exit 1
  144. coverage erase
  145. rm -rf "$ROOT_DIR/coverage/js" 2>/dev/null
  146. mkdir -p "$ROOT_DIR/coverage/js"
  147. # Enable Python subprocess coverage
  148. export COVERAGE_PROCESS_START="$ROOT_DIR/pyproject.toml"
  149. export PYTHONPATH="$ROOT_DIR:$PYTHONPATH" # For sitecustomize.py
  150. # Enable Node.js V8 coverage (built-in, no packages needed)
  151. export NODE_V8_COVERAGE="$ROOT_DIR/coverage/js"
  152. echo "Python coverage: enabled (subprocess support)"
  153. echo "JavaScript coverage: enabled (NODE_V8_COVERAGE=$NODE_V8_COVERAGE)"
  154. echo ""
  155. fi
  156. # Change to plugins directory
  157. cd "$ROOT_DIR/archivebox/plugins" || exit 1
  158. echo "=========================================="
  159. echo "ArchiveBox Plugin Tests"
  160. echo "=========================================="
  161. echo ""
  162. if [ -n "$PLUGIN_FILTER" ]; then
  163. echo "Filter: $PLUGIN_FILTER"
  164. else
  165. echo "Running all plugin tests"
  166. fi
  167. if [ "$ENABLE_COVERAGE" = true ]; then
  168. echo "Coverage: enabled"
  169. else
  170. echo "Coverage: disabled"
  171. fi
  172. echo ""
  173. # Track results
  174. TOTAL_PLUGINS=0
  175. PASSED_PLUGINS=0
  176. FAILED_PLUGINS=0
  177. # Find and run plugin tests
  178. if [ -n "$PLUGIN_FILTER" ]; then
  179. # Run tests for specific plugin(s) matching pattern
  180. TEST_DIRS=$(find . -maxdepth 2 -type d -path "./${PLUGIN_FILTER}*/tests" 2>/dev/null | sort)
  181. else
  182. # Run all plugin tests
  183. TEST_DIRS=$(find . -maxdepth 2 -type d -name "tests" -path "./*/tests" 2>/dev/null | sort)
  184. fi
  185. if [ -z "$TEST_DIRS" ]; then
  186. echo -e "${YELLOW}No plugin tests found${NC}"
  187. [ -n "$PLUGIN_FILTER" ] && echo "Pattern: $PLUGIN_FILTER"
  188. exit 0
  189. fi
  190. for test_dir in $TEST_DIRS; do
  191. # Check if there are any Python test files
  192. if ! compgen -G "${test_dir}/test_*.py" > /dev/null 2>&1; then
  193. continue
  194. fi
  195. plugin_name=$(basename $(dirname "$test_dir"))
  196. TOTAL_PLUGINS=$((TOTAL_PLUGINS + 1))
  197. echo -e "${YELLOW}[RUNNING]${NC} $plugin_name"
  198. # Build pytest command with optional coverage
  199. PYTEST_CMD="python -m pytest $test_dir -p no:django -v --tb=short"
  200. if [ "$ENABLE_COVERAGE" = true ]; then
  201. PYTEST_CMD="$PYTEST_CMD --cov=$plugin_name --cov-append --cov-branch"
  202. echo "[DEBUG] NODE_V8_COVERAGE before pytest: $NODE_V8_COVERAGE"
  203. python -c "import os; print('[DEBUG BASH->PYTHON] NODE_V8_COVERAGE:', os.environ.get('NODE_V8_COVERAGE', 'NOT_SET'))"
  204. fi
  205. if eval "$PYTEST_CMD" 2>&1 | grep -v "^platform\|^cachedir\|^rootdir\|^configfile\|^plugins:" | tail -100; then
  206. echo -e "${GREEN}[PASSED]${NC} $plugin_name"
  207. PASSED_PLUGINS=$((PASSED_PLUGINS + 1))
  208. else
  209. echo -e "${RED}[FAILED]${NC} $plugin_name"
  210. FAILED_PLUGINS=$((FAILED_PLUGINS + 1))
  211. fi
  212. echo ""
  213. done
  214. # Print summary
  215. echo "=========================================="
  216. echo "Test Summary"
  217. echo "=========================================="
  218. echo -e "Total plugins tested: $TOTAL_PLUGINS"
  219. echo -e "${GREEN}Passed:${NC} $PASSED_PLUGINS"
  220. echo -e "${RED}Failed:${NC} $FAILED_PLUGINS"
  221. echo ""
  222. if [ $TOTAL_PLUGINS -eq 0 ]; then
  223. echo -e "${YELLOW}⚠ No tests found${NC}"
  224. exit 0
  225. elif [ $FAILED_PLUGINS -eq 0 ]; then
  226. echo -e "${GREEN}✓ All plugin tests passed!${NC}"
  227. # Show coverage summary if enabled
  228. if [ "$ENABLE_COVERAGE" = true ]; then
  229. echo ""
  230. echo "=========================================="
  231. echo "Python Coverage Summary"
  232. echo "=========================================="
  233. # Coverage data is in ROOT_DIR, combine and report from there
  234. cd "$ROOT_DIR" || exit 1
  235. # Copy coverage data from plugins dir if it exists
  236. if [ -f "$ROOT_DIR/archivebox/plugins/.coverage" ]; then
  237. cp "$ROOT_DIR/archivebox/plugins/.coverage" "$ROOT_DIR/.coverage"
  238. fi
  239. coverage combine 2>/dev/null || true
  240. coverage report --include="archivebox/plugins/*" --omit="*/tests/*" 2>&1 | head -50
  241. echo ""
  242. echo "=========================================="
  243. echo "JavaScript Coverage Summary"
  244. echo "=========================================="
  245. show_js_coverage "$ROOT_DIR/coverage/js"
  246. echo ""
  247. echo "For detailed coverage reports (from project root):"
  248. echo " Python: coverage report --show-missing --include='archivebox/plugins/*' --omit='*/tests/*'"
  249. echo " Python: coverage json # LLM-friendly format"
  250. echo " Python: coverage html # Interactive HTML report"
  251. echo " JavaScript: ./bin/test_plugins.sh --coverage-report"
  252. fi
  253. exit 0
  254. else
  255. echo -e "${RED}✗ Some plugin tests failed${NC}"
  256. exit 1
  257. fi